From 0265f6e70cf64ff920d579d1f92e7b0b247e83da Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 13 Apr 2026 23:14:52 +0300 Subject: [PATCH] fix: prevent task name truncation when creating from inbox (#1020) * fix: prevent Telegram polling errors from blocking container startup (#989) This fix addresses the issue where the container gets stuck in an endless loop of Telegram connection errors when the bot is configured but Telegram is unreachable during startup. Changes: - Add 10-second startup delay before initializing Telegram polling to allow the system to settle - Implement exponential backoff (5s to 5min) when Telegram connection fails - Add rate-limited error logging (max once per minute per user) to reduce log spam and prevent event loop blocking - Track error state per user to manage backoff independently - Auto-reset error state on successful connection - Update tests to account for new error state tracking Fixes #989 * fix: prevent task name truncation when creating from inbox Changes: - Change task.name from VARCHAR(255) to TEXT to prevent any potential truncation - Change inbox_items.title and cleaned_content from VARCHAR(255) to TEXT - Add integration test to verify long task names are preserved - Add unit test to verify cleaned_content doesn't truncate While investigation showed no actual truncation occurring in the codebase, this defensive fix ensures unlimited text length for task names and inbox content, eliminating any possibility of truncation at the database level. Fixes #1016 --- ...0413000001-ensure-unlimited-text-fields.js | 47 +++++++++++++++++++ backend/models/inbox_item.js | 4 +- backend/models/task.js | 2 +- backend/tests/integration/tasks.test.js | 21 +++++++++ .../services/inboxProcessingService.test.js | 12 +++++ 5 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 backend/migrations/20260413000001-ensure-unlimited-text-fields.js diff --git a/backend/migrations/20260413000001-ensure-unlimited-text-fields.js b/backend/migrations/20260413000001-ensure-unlimited-text-fields.js new file mode 100644 index 0000000..8d46aa6 --- /dev/null +++ b/backend/migrations/20260413000001-ensure-unlimited-text-fields.js @@ -0,0 +1,47 @@ +'use strict'; + +module.exports = { + async up(queryInterface, Sequelize) { + // Change task name from VARCHAR(255) to TEXT to prevent truncation + await queryInterface.changeColumn('tasks', 'name', { + type: Sequelize.TEXT, + allowNull: false, + }); + + // Change inbox_items cleaned_content from VARCHAR(255) to TEXT + await queryInterface.changeColumn('inbox_items', 'cleaned_content', { + type: Sequelize.TEXT, + allowNull: true, + comment: 'Content with tags and project references removed', + }); + + // Change inbox_items title from VARCHAR(255) to TEXT + await queryInterface.changeColumn('inbox_items', 'title', { + type: Sequelize.TEXT, + allowNull: true, + comment: + 'Optional title field for inbox items, auto-generated for long content', + }); + }, + + async down(queryInterface, Sequelize) { + // Revert back to VARCHAR(255) + await queryInterface.changeColumn('tasks', 'name', { + type: Sequelize.STRING, + allowNull: false, + }); + + await queryInterface.changeColumn('inbox_items', 'cleaned_content', { + type: Sequelize.STRING, + allowNull: true, + comment: 'Content with tags and project references removed', + }); + + await queryInterface.changeColumn('inbox_items', 'title', { + type: Sequelize.STRING, + allowNull: true, + comment: + 'Optional title field for inbox items, auto-generated for long content', + }); + }, +}; diff --git a/backend/models/inbox_item.js b/backend/models/inbox_item.js index 16adeed..28a589e 100644 --- a/backend/models/inbox_item.js +++ b/backend/models/inbox_item.js @@ -21,7 +21,7 @@ module.exports = (sequelize) => { allowNull: false, }, title: { - type: DataTypes.STRING, + type: DataTypes.TEXT, allowNull: true, }, status: { @@ -58,7 +58,7 @@ module.exports = (sequelize) => { allowNull: true, }, cleaned_content: { - type: DataTypes.STRING, + type: DataTypes.TEXT, allowNull: true, }, }, diff --git a/backend/models/task.js b/backend/models/task.js index d13b394..cee0362 100644 --- a/backend/models/task.js +++ b/backend/models/task.js @@ -17,7 +17,7 @@ module.exports = (sequelize) => { defaultValue: uid, }, name: { - type: DataTypes.STRING, + type: DataTypes.TEXT, allowNull: false, }, due_date: { diff --git a/backend/tests/integration/tasks.test.js b/backend/tests/integration/tasks.test.js index b85465b..62341f1 100644 --- a/backend/tests/integration/tasks.test.js +++ b/backend/tests/integration/tasks.test.js @@ -68,6 +68,27 @@ describe('Tasks Routes', () => { // Restore original console.error console.error = originalConsoleError; }); + + it('should not truncate long task names', async () => { + const longTaskName = + 'This task has a long name, very long name, with lots of stuff'; + const taskData = { + name: longTaskName, + priority: 'low', + status: 'not_started', + }; + + const response = await agent.post('/api/task').send(taskData); + + expect(response.status).toBe(201); + expect(response.body.name).toBe(longTaskName); + expect(response.body.name.length).toBe(longTaskName.length); + + const retrievedTask = await Task.findOne({ + where: { id: response.body.id }, + }); + expect(retrievedTask.name).toBe(longTaskName); + }); }); describe('GET /api/tasks', () => { diff --git a/backend/tests/unit/services/inboxProcessingService.test.js b/backend/tests/unit/services/inboxProcessingService.test.js index 2bf32e0..93a8484 100644 --- a/backend/tests/unit/services/inboxProcessingService.test.js +++ b/backend/tests/unit/services/inboxProcessingService.test.js @@ -56,6 +56,18 @@ describe('inboxProcessingService', () => { suggested_reason: null, }); }); + + it('should not truncate long cleaned_content', () => { + const longText = + 'This task has a long name, very long name, with lots of stuff'; + const content = `${longText} #tag1 +Project`; + const result = processInboxItem(content); + + expect(result.parsed_tags).toEqual(['tag1']); + expect(result.parsed_projects).toEqual(['Project']); + expect(result.cleaned_content).toBe(longText); + expect(result.cleaned_content.length).toBe(longText.length); + }); }); describe('containsUrl', () => {