@teamvibe/poller 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.d.ts +8 -8
- package/dist/config.js +3 -3
- package/dist/heartbeat-manager.d.ts +5 -3
- package/dist/heartbeat-manager.js +79 -68
- package/dist/logger.js +28 -22
- package/dist/poller.js +1 -1
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
- package/dist/control-handler.test.d.ts +0 -1
- package/dist/control-handler.test.js +0 -74
package/dist/config.d.ts
CHANGED
|
@@ -27,8 +27,8 @@ declare const configSchema: z.ZodObject<{
|
|
|
27
27
|
BASE_BRAIN_PATH: z.ZodDefault<z.ZodString>;
|
|
28
28
|
BRAIN_AUTO_UPDATE: z.ZodEffects<z.ZodDefault<z.ZodString>, boolean, string | undefined>;
|
|
29
29
|
BRAIN_UPDATE_COOLDOWN_MS: z.ZodDefault<z.ZodNumber>;
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
HEARTBEAT_CRON: z.ZodDefault<z.ZodString>;
|
|
31
|
+
MAINTENANCE_CRON: z.ZodDefault<z.ZodString>;
|
|
32
32
|
MAINTENANCE_ENABLED: z.ZodEffects<z.ZodDefault<z.ZodString>, boolean, string | undefined>;
|
|
33
33
|
AUTO_UPDATE_POLICY: z.ZodDefault<z.ZodEnum<["patch", "minor", "off"]>>;
|
|
34
34
|
AUTO_UPDATE_SCHEDULE: z.ZodDefault<z.ZodString>;
|
|
@@ -56,8 +56,8 @@ declare const configSchema: z.ZodObject<{
|
|
|
56
56
|
BASE_BRAIN_PATH: string;
|
|
57
57
|
BRAIN_AUTO_UPDATE: boolean;
|
|
58
58
|
BRAIN_UPDATE_COOLDOWN_MS: number;
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
HEARTBEAT_CRON: string;
|
|
60
|
+
MAINTENANCE_CRON: string;
|
|
61
61
|
MAINTENANCE_ENABLED: boolean;
|
|
62
62
|
AUTO_UPDATE_POLICY: "patch" | "minor" | "off";
|
|
63
63
|
AUTO_UPDATE_SCHEDULE: string;
|
|
@@ -91,8 +91,8 @@ declare const configSchema: z.ZodObject<{
|
|
|
91
91
|
BASE_BRAIN_PATH?: string | undefined;
|
|
92
92
|
BRAIN_AUTO_UPDATE?: string | undefined;
|
|
93
93
|
BRAIN_UPDATE_COOLDOWN_MS?: number | undefined;
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
HEARTBEAT_CRON?: string | undefined;
|
|
95
|
+
MAINTENANCE_CRON?: string | undefined;
|
|
96
96
|
MAINTENANCE_ENABLED?: string | undefined;
|
|
97
97
|
AUTO_UPDATE_POLICY?: "patch" | "minor" | "off" | undefined;
|
|
98
98
|
AUTO_UPDATE_SCHEDULE?: string | undefined;
|
|
@@ -123,8 +123,8 @@ export declare const config: {
|
|
|
123
123
|
BASE_BRAIN_PATH: string;
|
|
124
124
|
BRAIN_AUTO_UPDATE: boolean;
|
|
125
125
|
BRAIN_UPDATE_COOLDOWN_MS: number;
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
HEARTBEAT_CRON: string;
|
|
127
|
+
MAINTENANCE_CRON: string;
|
|
128
128
|
MAINTENANCE_ENABLED: boolean;
|
|
129
129
|
AUTO_UPDATE_POLICY: "patch" | "minor" | "off";
|
|
130
130
|
AUTO_UPDATE_SCHEDULE: string;
|
package/dist/config.js
CHANGED
|
@@ -42,9 +42,9 @@ const configSchema = z.object({
|
|
|
42
42
|
.default('true')
|
|
43
43
|
.transform((v) => v.toLowerCase() === 'true'),
|
|
44
44
|
BRAIN_UPDATE_COOLDOWN_MS: z.coerce.number().default(300000), // 5 minutes
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
// Brain heartbeat & maintenance (cron-based)
|
|
46
|
+
HEARTBEAT_CRON: z.string().default('*/5 * * * *'), // Every 5 minutes
|
|
47
|
+
MAINTENANCE_CRON: z.string().default('30 3 * * *'), // Daily at 03:30 UTC
|
|
48
48
|
MAINTENANCE_ENABLED: z.string().default('true').transform((v) => v.toLowerCase() === 'true'),
|
|
49
49
|
// Auto-update settings
|
|
50
50
|
AUTO_UPDATE_POLICY: z.enum(['patch', 'minor', 'off']).default('patch'),
|
|
@@ -5,8 +5,10 @@ import type { TeamVibeQueueMessage } from './types.js';
|
|
|
5
5
|
*/
|
|
6
6
|
export declare function registerBrain(msg: TeamVibeQueueMessage): void;
|
|
7
7
|
/**
|
|
8
|
-
* Start the
|
|
9
|
-
*
|
|
8
|
+
* Start the cron-based heartbeat and maintenance schedules.
|
|
9
|
+
*
|
|
10
|
+
* - Heartbeat: lightweight, frequent (default: every 5 min)
|
|
11
|
+
* - Maintenance: heavy, daily (default: 03:30 UTC)
|
|
10
12
|
*/
|
|
11
13
|
export declare function startHeartbeatLoop(processMessageFn: (msg: {
|
|
12
14
|
queueMessage: TeamVibeQueueMessage;
|
|
@@ -14,6 +16,6 @@ export declare function startHeartbeatLoop(processMessageFn: (msg: {
|
|
|
14
16
|
messageId: string;
|
|
15
17
|
}) => Promise<void>): void;
|
|
16
18
|
/**
|
|
17
|
-
* Stop the heartbeat
|
|
19
|
+
* Stop the heartbeat and maintenance crons (for shutdown).
|
|
18
20
|
*/
|
|
19
21
|
export declare function stopHeartbeatLoop(): void;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { Cron } from 'croner';
|
|
1
2
|
import { config } from './config.js';
|
|
2
3
|
import { logger } from './logger.js';
|
|
3
4
|
import { isAtCapacity } from './claude-spawner.js';
|
|
4
5
|
import { emitError } from './event-queue.js';
|
|
5
6
|
const trackedBrains = new Map();
|
|
6
|
-
let
|
|
7
|
+
let heartbeatJob = null;
|
|
8
|
+
let maintenanceJob = null;
|
|
7
9
|
/**
|
|
8
10
|
* Register a brain from a real message so we know about it for heartbeats.
|
|
9
11
|
* Called from processMessage() after brain path is resolved.
|
|
@@ -12,11 +14,9 @@ export function registerBrain(msg) {
|
|
|
12
14
|
const brain = msg.teamvibe.brain;
|
|
13
15
|
if (!brain?.brainId)
|
|
14
16
|
return;
|
|
15
|
-
// Don't register from heartbeat messages (avoid self-referential loop)
|
|
16
|
-
if (msg.source === 'heartbeat')
|
|
17
|
+
// Don't register from heartbeat/maintenance messages (avoid self-referential loop)
|
|
18
|
+
if (msg.source === 'heartbeat' || msg.source === 'maintenance')
|
|
17
19
|
return;
|
|
18
|
-
const existing = trackedBrains.get(brain.brainId);
|
|
19
|
-
// Update brain info but preserve lastHeartbeat if it exists
|
|
20
20
|
trackedBrains.set(brain.brainId, {
|
|
21
21
|
brainId: brain.brainId,
|
|
22
22
|
gitRepoUrl: brain.gitRepoUrl,
|
|
@@ -25,82 +25,93 @@ export function registerBrain(msg) {
|
|
|
25
25
|
botId: msg.teamvibe.botId,
|
|
26
26
|
botToken: msg.teamvibe.botToken,
|
|
27
27
|
channelId: msg.teamvibe.channelId,
|
|
28
|
-
lastHeartbeat: existing?.lastHeartbeat ?? 0,
|
|
29
28
|
});
|
|
30
29
|
logger.debug(`[Heartbeat] Registered brain: ${brain.brainId}`);
|
|
31
30
|
}
|
|
32
31
|
/**
|
|
33
|
-
*
|
|
34
|
-
|
|
32
|
+
* Send a synthetic message to all tracked brains.
|
|
33
|
+
*/
|
|
34
|
+
async function triggerAllBrains(source, processMessageFn) {
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
for (const [brainId, info] of trackedBrains.entries()) {
|
|
37
|
+
if (isAtCapacity()) {
|
|
38
|
+
logger.debug(`[${source}] At capacity, skipping ${brainId}`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
logger.info(`[${source}] Triggering ${source} for brain ${brainId}`);
|
|
42
|
+
const syntheticMessage = {
|
|
43
|
+
id: `${source}_${brainId}_${now}`,
|
|
44
|
+
source,
|
|
45
|
+
thread_id: `${source}_${brainId}_${now}`,
|
|
46
|
+
sender: {
|
|
47
|
+
id: 'system',
|
|
48
|
+
name: 'System',
|
|
49
|
+
},
|
|
50
|
+
text: source,
|
|
51
|
+
attachments: [],
|
|
52
|
+
response_context: {},
|
|
53
|
+
type: 'message',
|
|
54
|
+
teamvibe: {
|
|
55
|
+
workspaceId: info.workspaceId,
|
|
56
|
+
botId: info.botId,
|
|
57
|
+
botToken: info.botToken,
|
|
58
|
+
channelId: info.channelId,
|
|
59
|
+
brain: {
|
|
60
|
+
brainId: info.brainId,
|
|
61
|
+
gitRepoUrl: info.gitRepoUrl,
|
|
62
|
+
branch: info.branch,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
try {
|
|
67
|
+
await processMessageFn({
|
|
68
|
+
queueMessage: syntheticMessage,
|
|
69
|
+
receiptHandle: '', // No SQS receipt for synthetic messages
|
|
70
|
+
messageId: syntheticMessage.id,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
logger.error(`[${source}] Failed for ${brainId}: ${error instanceof Error ? error.message : error}`);
|
|
75
|
+
emitError('session', `${source} failed for ${brainId}: ${error instanceof Error ? error.message : String(error)}`, {
|
|
76
|
+
channelId: info.channelId,
|
|
77
|
+
context: { brainId },
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Start the cron-based heartbeat and maintenance schedules.
|
|
84
|
+
*
|
|
85
|
+
* - Heartbeat: lightweight, frequent (default: every 5 min)
|
|
86
|
+
* - Maintenance: heavy, daily (default: 03:30 UTC)
|
|
35
87
|
*/
|
|
36
88
|
export function startHeartbeatLoop(processMessageFn) {
|
|
37
89
|
if (!config.MAINTENANCE_ENABLED) {
|
|
38
90
|
logger.info('[Heartbeat] Maintenance disabled via config');
|
|
39
91
|
return;
|
|
40
92
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
logger.info(`[Heartbeat] Triggering maintenance for brain ${brainId} (last: ${info.lastHeartbeat === 0 ? 'never' : `${Math.round(elapsed / 60000)}m ago`})`);
|
|
54
|
-
// Update lastHeartbeat immediately to prevent double-triggering
|
|
55
|
-
info.lastHeartbeat = now;
|
|
56
|
-
const syntheticMessage = {
|
|
57
|
-
id: `heartbeat_${brainId}_${now}`,
|
|
58
|
-
source: 'heartbeat',
|
|
59
|
-
thread_id: `heartbeat_${brainId}_${now}`,
|
|
60
|
-
sender: {
|
|
61
|
-
id: 'system',
|
|
62
|
-
name: 'System',
|
|
63
|
-
},
|
|
64
|
-
text: 'heartbeat',
|
|
65
|
-
attachments: [],
|
|
66
|
-
response_context: {},
|
|
67
|
-
type: 'message',
|
|
68
|
-
teamvibe: {
|
|
69
|
-
workspaceId: info.workspaceId,
|
|
70
|
-
botId: info.botId,
|
|
71
|
-
botToken: info.botToken,
|
|
72
|
-
channelId: info.channelId,
|
|
73
|
-
brain: {
|
|
74
|
-
brainId: info.brainId,
|
|
75
|
-
gitRepoUrl: info.gitRepoUrl,
|
|
76
|
-
branch: info.branch,
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
try {
|
|
81
|
-
await processMessageFn({
|
|
82
|
-
queueMessage: syntheticMessage,
|
|
83
|
-
receiptHandle: '', // No SQS receipt for synthetic messages
|
|
84
|
-
messageId: syntheticMessage.id,
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
catch (error) {
|
|
88
|
-
logger.error(`[Heartbeat] Failed maintenance for ${brainId}: ${error instanceof Error ? error.message : error}`);
|
|
89
|
-
emitError('session', `Heartbeat maintenance failed for ${brainId}: ${error instanceof Error ? error.message : String(error)}`, {
|
|
90
|
-
channelId: info.channelId,
|
|
91
|
-
context: { brainId },
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}, config.MAINTENANCE_CHECK_INTERVAL_MS);
|
|
93
|
+
// Heartbeat cron — frequent, lightweight
|
|
94
|
+
logger.info(`[Heartbeat] Starting heartbeat cron: "${config.HEARTBEAT_CRON}"`);
|
|
95
|
+
heartbeatJob = new Cron(config.HEARTBEAT_CRON, { timezone: 'UTC', protect: true }, async () => {
|
|
96
|
+
await triggerAllBrains('heartbeat', processMessageFn);
|
|
97
|
+
});
|
|
98
|
+
// Maintenance cron — daily, heavy (consolidation, memory cleanup)
|
|
99
|
+
logger.info(`[Maintenance] Starting maintenance cron: "${config.MAINTENANCE_CRON}"`);
|
|
100
|
+
maintenanceJob = new Cron(config.MAINTENANCE_CRON, { timezone: 'UTC', protect: true }, async () => {
|
|
101
|
+
await triggerAllBrains('maintenance', processMessageFn);
|
|
102
|
+
});
|
|
96
103
|
}
|
|
97
104
|
/**
|
|
98
|
-
* Stop the heartbeat
|
|
105
|
+
* Stop the heartbeat and maintenance crons (for shutdown).
|
|
99
106
|
*/
|
|
100
107
|
export function stopHeartbeatLoop() {
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
108
|
+
if (heartbeatJob) {
|
|
109
|
+
heartbeatJob.stop();
|
|
110
|
+
heartbeatJob = null;
|
|
111
|
+
}
|
|
112
|
+
if (maintenanceJob) {
|
|
113
|
+
maintenanceJob.stop();
|
|
114
|
+
maintenanceJob = null;
|
|
105
115
|
}
|
|
116
|
+
logger.info('[Heartbeat] Stopped heartbeat & maintenance crons');
|
|
106
117
|
}
|
package/dist/logger.js
CHANGED
|
@@ -14,8 +14,8 @@ if (!existsSync(SESSIONS_DIR)) {
|
|
|
14
14
|
function formatDate(date) {
|
|
15
15
|
return date.toISOString();
|
|
16
16
|
}
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
const now = () => formatDate(new Date());
|
|
18
|
+
function writeToFile(level, message, timestamp, ...args) {
|
|
19
19
|
const formattedArgs = args
|
|
20
20
|
.map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
|
|
21
21
|
.join(' ');
|
|
@@ -35,8 +35,7 @@ export class SessionLogger {
|
|
|
35
35
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
36
36
|
this.logFile = join(SESSIONS_DIR, `${timestamp}_${sessionId}.log`);
|
|
37
37
|
}
|
|
38
|
-
write(level, message) {
|
|
39
|
-
const timestamp = formatDate(new Date());
|
|
38
|
+
write(level, message, timestamp) {
|
|
40
39
|
const logLine = `[${timestamp}] [${level}] ${message}\n`;
|
|
41
40
|
try {
|
|
42
41
|
appendFileSync(this.logFile, logLine);
|
|
@@ -46,27 +45,30 @@ export class SessionLogger {
|
|
|
46
45
|
}
|
|
47
46
|
}
|
|
48
47
|
info(message) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
const ts = now();
|
|
49
|
+
console.log(`[${ts}] [${this.sessionId}] ${message}`);
|
|
50
|
+
this.write('INFO', message, ts);
|
|
51
|
+
writeToFile('INFO', `[${this.sessionId}] ${message}`, ts);
|
|
52
52
|
}
|
|
53
53
|
error(message) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
const ts = now();
|
|
55
|
+
console.error(`[${ts}] [${this.sessionId}] ${message}`);
|
|
56
|
+
this.write('ERROR', message, ts);
|
|
57
|
+
writeToFile('ERROR', `[${this.sessionId}] ${message}`, ts);
|
|
57
58
|
}
|
|
58
59
|
claude(stream, message) {
|
|
60
|
+
const ts = now();
|
|
59
61
|
const prefix = `[Claude ${stream}]`;
|
|
60
62
|
if (stream === 'stderr') {
|
|
61
|
-
console.error(`[${this.sessionId}] ${prefix}`, message);
|
|
63
|
+
console.error(`[${ts}] [${this.sessionId}] ${prefix}`, message);
|
|
62
64
|
}
|
|
63
65
|
else {
|
|
64
|
-
console.log(`[${this.sessionId}] ${prefix}`, message);
|
|
66
|
+
console.log(`[${ts}] [${this.sessionId}] ${prefix}`, message);
|
|
65
67
|
}
|
|
66
|
-
this.write(`CLAUDE_${stream.toUpperCase()}`, message);
|
|
68
|
+
this.write(`CLAUDE_${stream.toUpperCase()}`, message, ts);
|
|
67
69
|
}
|
|
68
70
|
logPrompt(prompt) {
|
|
69
|
-
this.write('PROMPT', `\n${'='.repeat(50)}\n${prompt}\n${'='.repeat(50)}
|
|
71
|
+
this.write('PROMPT', `\n${'='.repeat(50)}\n${prompt}\n${'='.repeat(50)}`, now());
|
|
70
72
|
}
|
|
71
73
|
getLogFile() {
|
|
72
74
|
return this.logFile;
|
|
@@ -74,22 +76,26 @@ export class SessionLogger {
|
|
|
74
76
|
}
|
|
75
77
|
export const logger = {
|
|
76
78
|
info(message, ...args) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
const ts = now();
|
|
80
|
+
console.log(`[${ts}] [INFO] ${message}`, ...args);
|
|
81
|
+
writeToFile('INFO', message, ts, ...args);
|
|
79
82
|
},
|
|
80
83
|
error(message, ...args) {
|
|
81
|
-
|
|
82
|
-
|
|
84
|
+
const ts = now();
|
|
85
|
+
console.error(`[${ts}] [ERROR] ${message}`, ...args);
|
|
86
|
+
writeToFile('ERROR', message, ts, ...args);
|
|
83
87
|
},
|
|
84
88
|
warn(message, ...args) {
|
|
85
|
-
|
|
86
|
-
|
|
89
|
+
const ts = now();
|
|
90
|
+
console.warn(`[${ts}] [WARN] ${message}`, ...args);
|
|
91
|
+
writeToFile('WARN', message, ts, ...args);
|
|
87
92
|
},
|
|
88
93
|
debug(message, ...args) {
|
|
94
|
+
const ts = now();
|
|
89
95
|
if (process.env['DEBUG']) {
|
|
90
|
-
console.debug(message
|
|
96
|
+
console.debug(`[${ts}] [DEBUG] ${message}`, ...args);
|
|
91
97
|
}
|
|
92
|
-
writeToFile('DEBUG', message, ...args);
|
|
98
|
+
writeToFile('DEBUG', message, ts, ...args);
|
|
93
99
|
},
|
|
94
100
|
createSession(sessionId) {
|
|
95
101
|
return new SessionLogger(sessionId);
|
package/dist/poller.js
CHANGED
|
@@ -200,7 +200,7 @@ async function processMessage(received) {
|
|
|
200
200
|
const threadId = queueMessage.thread_id;
|
|
201
201
|
const sessionLog = logger.createSession(logSessionId);
|
|
202
202
|
// Fetch user info if we only have ID
|
|
203
|
-
if (queueMessage.source !== 'cron' && queueMessage.source !== 'heartbeat' &&
|
|
203
|
+
if (queueMessage.source !== 'cron' && queueMessage.source !== 'heartbeat' && queueMessage.source !== 'maintenance' &&
|
|
204
204
|
queueMessage.sender.name === 'Unknown User' &&
|
|
205
205
|
queueMessage.sender.id !== 'unknown') {
|
|
206
206
|
const userInfo = await getUserInfo(queueMessage.teamvibe.botToken, queueMessage.sender.id);
|
package/dist/types.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export interface ControlMessage {
|
|
|
7
7
|
export type QueueMessage = TeamVibeQueueMessage | ControlMessage;
|
|
8
8
|
export interface TeamVibeQueueMessage {
|
|
9
9
|
id: string;
|
|
10
|
-
source: 'slack' | 'email' | 'cron' | 'heartbeat' | 'webhook';
|
|
10
|
+
source: 'slack' | 'email' | 'cron' | 'heartbeat' | 'maintenance' | 'webhook';
|
|
11
11
|
thread_id: string;
|
|
12
12
|
sender: {
|
|
13
13
|
id: string;
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from 'bun:test';
|
|
2
|
-
import { isControlMessage } from './control-handler.js';
|
|
3
|
-
import { classifyUpdate } from './auto-update.js';
|
|
4
|
-
describe('isControlMessage', () => {
|
|
5
|
-
test('valid restart message', () => {
|
|
6
|
-
expect(isControlMessage({ source: 'control', action: 'restart' })).toBe(true);
|
|
7
|
-
});
|
|
8
|
-
test('valid update message', () => {
|
|
9
|
-
expect(isControlMessage({ source: 'control', action: 'update' })).toBe(true);
|
|
10
|
-
});
|
|
11
|
-
test('valid update with targetVersion', () => {
|
|
12
|
-
expect(isControlMessage({ source: 'control', action: 'update', targetVersion: '1.2.3' })).toBe(true);
|
|
13
|
-
});
|
|
14
|
-
test('rejects non-control source', () => {
|
|
15
|
-
expect(isControlMessage({ source: 'slack', action: 'restart' })).toBe(false);
|
|
16
|
-
});
|
|
17
|
-
test('rejects unknown action', () => {
|
|
18
|
-
expect(isControlMessage({ source: 'control', action: 'delete' })).toBe(false);
|
|
19
|
-
});
|
|
20
|
-
test('rejects null', () => {
|
|
21
|
-
expect(isControlMessage(null)).toBe(false);
|
|
22
|
-
});
|
|
23
|
-
test('rejects undefined', () => {
|
|
24
|
-
expect(isControlMessage(undefined)).toBe(false);
|
|
25
|
-
});
|
|
26
|
-
test('rejects string', () => {
|
|
27
|
-
expect(isControlMessage('control')).toBe(false);
|
|
28
|
-
});
|
|
29
|
-
test('rejects empty object', () => {
|
|
30
|
-
expect(isControlMessage({})).toBe(false);
|
|
31
|
-
});
|
|
32
|
-
test('rejects regular queue message', () => {
|
|
33
|
-
expect(isControlMessage({
|
|
34
|
-
id: '123',
|
|
35
|
-
source: 'cron',
|
|
36
|
-
thread_id: 'thread1',
|
|
37
|
-
sender: { id: 'user1', name: 'Test' },
|
|
38
|
-
text: 'hello',
|
|
39
|
-
})).toBe(false);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
describe('classifyUpdate', () => {
|
|
43
|
-
test('detects patch update', () => {
|
|
44
|
-
expect(classifyUpdate('1.2.3', '1.2.4')).toBe('patch');
|
|
45
|
-
});
|
|
46
|
-
test('detects minor update', () => {
|
|
47
|
-
expect(classifyUpdate('1.2.3', '1.3.0')).toBe('minor');
|
|
48
|
-
});
|
|
49
|
-
test('detects major update', () => {
|
|
50
|
-
expect(classifyUpdate('1.2.3', '2.0.0')).toBe('major');
|
|
51
|
-
});
|
|
52
|
-
test('returns null for same version', () => {
|
|
53
|
-
expect(classifyUpdate('1.2.3', '1.2.3')).toBeNull();
|
|
54
|
-
});
|
|
55
|
-
test('returns null for downgrade (patch)', () => {
|
|
56
|
-
expect(classifyUpdate('1.2.4', '1.2.3')).toBeNull();
|
|
57
|
-
});
|
|
58
|
-
test('returns null for downgrade (minor)', () => {
|
|
59
|
-
expect(classifyUpdate('1.3.0', '1.2.5')).toBeNull();
|
|
60
|
-
});
|
|
61
|
-
test('returns null for downgrade (major)', () => {
|
|
62
|
-
expect(classifyUpdate('2.0.0', '1.9.9')).toBeNull();
|
|
63
|
-
});
|
|
64
|
-
test('handles zero versions', () => {
|
|
65
|
-
expect(classifyUpdate('0.0.1', '0.0.2')).toBe('patch');
|
|
66
|
-
expect(classifyUpdate('0.1.0', '0.2.0')).toBe('minor');
|
|
67
|
-
});
|
|
68
|
-
test('minor update with different patch', () => {
|
|
69
|
-
expect(classifyUpdate('1.2.3', '1.3.5')).toBe('minor');
|
|
70
|
-
});
|
|
71
|
-
test('major update with different minor/patch', () => {
|
|
72
|
-
expect(classifyUpdate('1.2.3', '2.1.0')).toBe('major');
|
|
73
|
-
});
|
|
74
|
-
});
|