comfy-pr 0.2.26 → 1.0.1
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/README.md +258 -157
- package/bot/IdleWaiter.ts +31 -0
- package/bot/RestartManager.spec.ts +163 -0
- package/bot/RestartManager.ts +190 -0
- package/bot/WorkingTasksManager.spec.ts +154 -0
- package/bot/cli.ts +1171 -0
- package/bot/codesearch-ai-wip.ts +351 -0
- package/bot/error-collector.ts +133 -0
- package/bot/index.ts +14 -0
- package/bot/restart-example.ts +99 -0
- package/bot/slack-bolt.ts +688 -0
- package/bot/slack-bot.ts +1575 -0
- package/bot/state.ts +19 -0
- package/bot/templateLoader.test.ts +84 -0
- package/bot/templateLoader.ts +92 -0
- package/next.config.ts +26 -0
- package/package.json +161 -41
- package/post-summary.ts +44 -0
- package/src/Authors.ts +2 -2
- package/src/CMNodes.ts +3 -3
- package/src/CNRepos.ts +30 -7
- package/src/CRNodes.ts +11 -9
- package/src/CRPulls.ts +3 -0
- package/src/EmailTasks.ts +27 -9
- package/src/FORK_OWNER.ts +3 -1
- package/src/FollowRules.ts +3 -3
- package/src/GithubIssueComments.ts +1 -1
- package/src/Totals.ts +9 -8
- package/src/WorkerInstances.ts +31 -8
- package/src/addCommentAction.ts +19 -11
- package/src/analyzePullsStatus.ts +39 -17
- package/src/analyzeTotals.ts +19 -11
- package/src/bypassRepos.ts +6 -5
- package/src/checkPRsFailures.ts +13 -8
- package/src/cli.test.ts +2 -0
- package/src/cli.ts +1 -1
- package/src/constants.ts +2 -0
- package/src/createComfyRegistryPRsFromCandidates.ts +20 -12
- package/src/createComfyRegistryPullRequests.ts +35 -10
- package/src/createGithubForkForRepo.ts +36 -11
- package/src/createGithubPullRequest.ts +83 -33
- package/src/createIssueComment.ts +4 -4
- package/src/fetchComfyRegistryNodes.ts +34 -4
- package/src/fetchCurrentGeoInfo.ts +3 -1
- package/src/fetchRelatedPullWithComments.ts +1 -1
- package/src/fetchRepoDescriptionMap.ts +1 -4
- package/src/followRuleSchema.ts +10 -4
- package/src/getBranchWorkingDir.ts +3 -7
- package/src/getRepoWorkingDir.ts +2 -3
- package/src/gh.spec.ts +571 -0
- package/src/ghData.ts +13 -0
- package/src/ghPageFlow.ts +33 -0
- package/src/ghSubscriber.ts +76 -0
- package/src/ghUser.ts +28 -4
- package/src/index.ts +13 -4
- package/src/initializeFollowRules.ts +11 -4
- package/src/logger.spec.ts +95 -0
- package/src/logger.ts +46 -0
- package/src/makeGpl3LicenseBranch.ts +66 -0
- package/src/makePublishBranch.ts +21 -15
- package/src/makeTomlBranch.ts +48 -14
- package/src/makeUpdateTomlLicenseBranch.ts +40 -44
- package/src/matchRelatedPulls.ts +9 -4
- package/src/normalizeGithubUrl.spec.ts +131 -0
- package/src/normalizeGithubUrl.ts +64 -0
- package/src/parseIssueUrl.spec.ts +95 -0
- package/src/parseIssueUrl.ts +17 -3
- package/src/parseOwnerRepo.spec.ts +147 -0
- package/src/parseOwnerRepo.ts +8 -5
- package/src/parsePullsState.ts +2 -2
- package/src/parseTitleBodyOfMarkdown.ts +1 -1
- package/src/pickRepoInfo.ts +37 -0
- package/src/postSlackMessage.ts +5 -1
- package/src/preload.ts +30 -27
- package/src/readTemplateTitle.ts +1 -3
- package/src/sendEmailAction.ts +14 -10
- package/src/sendGmail.ts +3 -3
- package/src/updateAuthorsForGithub.ts +53 -35
- package/src/updateAuthorsFromCNRepo.ts +12 -5
- package/src/updateCMNodesDuplicationWarnings.ts +14 -9
- package/src/updateCMRepos.ts +1 -1
- package/src/updateCNRepos.ts +14 -14
- package/src/updateCNReposCRPullsComments.ts +7 -5
- package/src/updateCNReposInfo.ts +49 -40
- package/src/updateCNReposPRCandidate.ts +3 -3
- package/src/updateCNReposPulls.ts +9 -5
- package/src/updateCNReposPullsDashboard.ts +11 -9
- package/src/updateCNReposRelatedPulls.ts +3 -3
- package/src/updateCRNodes.ts +24 -5
- package/src/updateCRRepos.ts +1 -1
- package/src/updateComfyTotals.ts +7 -5
- package/src/updateFollowRuleSet.ts +17 -9
- package/src/updateOutdatedPullsTemplates.ts +61 -48
- package/src/updateSlackMessages.ts +8 -4
- package/tailwind.config.ts +7 -1
- package/next-env.d.ts +0 -5
- package/src/GIT_USEREMAIL.ts +0 -5
- package/src/GIT_USERNAME.ts +0 -9
- package/src/clone_modify_push_Branches.ts +0 -21
- package/src/tomlFillDescription.ts +0 -17
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach, mock } from "bun:test";
|
|
2
|
+
import { RestartManager } from "./RestartManager";
|
|
3
|
+
import { writeFile, mkdir, rm } from "fs/promises";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
|
|
6
|
+
describe("RestartManager", () => {
|
|
7
|
+
let testDir: string;
|
|
8
|
+
let restartManager: RestartManager;
|
|
9
|
+
let isIdleMock: ReturnType<typeof mock>;
|
|
10
|
+
let onRestartMock: ReturnType<typeof mock>;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
// Create a temporary test directory
|
|
14
|
+
testDir = join(process.cwd(), ".test-restart-manager");
|
|
15
|
+
await mkdir(testDir, { recursive: true });
|
|
16
|
+
|
|
17
|
+
// Create mocks
|
|
18
|
+
isIdleMock = mock(() => true);
|
|
19
|
+
onRestartMock = mock(() => {});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
// Clean up
|
|
24
|
+
if (restartManager) {
|
|
25
|
+
restartManager.stop();
|
|
26
|
+
}
|
|
27
|
+
await rm(testDir, { recursive: true, force: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("should start watching directories", () => {
|
|
31
|
+
restartManager = new RestartManager({
|
|
32
|
+
watchPaths: [testDir],
|
|
33
|
+
isIdle: isIdleMock,
|
|
34
|
+
onRestart: onRestartMock,
|
|
35
|
+
debounceDelay: 100,
|
|
36
|
+
idleCheckInterval: 100,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
restartManager.start();
|
|
40
|
+
// If no error is thrown, the test passes
|
|
41
|
+
expect(true).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("should queue restart when file changes and bot is idle", async () => {
|
|
45
|
+
restartManager = new RestartManager({
|
|
46
|
+
watchPaths: [testDir],
|
|
47
|
+
isIdle: isIdleMock,
|
|
48
|
+
onRestart: onRestartMock,
|
|
49
|
+
debounceDelay: 100,
|
|
50
|
+
idleCheckInterval: 100,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
restartManager.start();
|
|
54
|
+
|
|
55
|
+
// Manually trigger file change (simulating what fs.watch would do)
|
|
56
|
+
// @ts-ignore - accessing private method for testing
|
|
57
|
+
restartManager.onFileChange("test.ts");
|
|
58
|
+
|
|
59
|
+
// Wait for debounce + idle check
|
|
60
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
61
|
+
|
|
62
|
+
// Should have called onRestart since bot is idle
|
|
63
|
+
expect(onRestartMock).toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("should wait for idle state before restarting", async () => {
|
|
67
|
+
let isIdle = false;
|
|
68
|
+
isIdleMock = mock(() => isIdle);
|
|
69
|
+
|
|
70
|
+
restartManager = new RestartManager({
|
|
71
|
+
watchPaths: [testDir],
|
|
72
|
+
isIdle: isIdleMock,
|
|
73
|
+
onRestart: onRestartMock,
|
|
74
|
+
debounceDelay: 100,
|
|
75
|
+
idleCheckInterval: 100,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
restartManager.start();
|
|
79
|
+
|
|
80
|
+
// Manually trigger file change
|
|
81
|
+
// @ts-ignore - accessing private method for testing
|
|
82
|
+
restartManager.onFileChange("test.ts");
|
|
83
|
+
|
|
84
|
+
// Wait for debounce
|
|
85
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
86
|
+
|
|
87
|
+
// Should not have restarted yet (bot is busy)
|
|
88
|
+
expect(onRestartMock).not.toHaveBeenCalled();
|
|
89
|
+
|
|
90
|
+
// Make bot idle
|
|
91
|
+
isIdle = true;
|
|
92
|
+
|
|
93
|
+
// Wait for idle check
|
|
94
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
95
|
+
|
|
96
|
+
// Should have restarted now
|
|
97
|
+
expect(onRestartMock).toHaveBeenCalled();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("should ignore markdown files", async () => {
|
|
101
|
+
restartManager = new RestartManager({
|
|
102
|
+
watchPaths: [testDir],
|
|
103
|
+
isIdle: isIdleMock,
|
|
104
|
+
onRestart: onRestartMock,
|
|
105
|
+
debounceDelay: 100,
|
|
106
|
+
idleCheckInterval: 100,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
restartManager.start();
|
|
110
|
+
|
|
111
|
+
// Create a markdown file (should be ignored)
|
|
112
|
+
const mdFile = join(testDir, "README.md");
|
|
113
|
+
await writeFile(mdFile, "# Test");
|
|
114
|
+
|
|
115
|
+
// Wait for debounce + idle check
|
|
116
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
117
|
+
|
|
118
|
+
// Should NOT have called onRestart (markdown files are ignored)
|
|
119
|
+
expect(onRestartMock).not.toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("should debounce multiple file changes", async () => {
|
|
123
|
+
restartManager = new RestartManager({
|
|
124
|
+
watchPaths: [testDir],
|
|
125
|
+
isIdle: isIdleMock,
|
|
126
|
+
onRestart: onRestartMock,
|
|
127
|
+
debounceDelay: 200,
|
|
128
|
+
idleCheckInterval: 100,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
restartManager.start();
|
|
132
|
+
|
|
133
|
+
// Trigger multiple file changes quickly
|
|
134
|
+
// @ts-ignore - accessing private method for testing
|
|
135
|
+
restartManager.onFileChange("test1.ts");
|
|
136
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
137
|
+
// @ts-ignore
|
|
138
|
+
restartManager.onFileChange("test2.ts");
|
|
139
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
140
|
+
// @ts-ignore
|
|
141
|
+
restartManager.onFileChange("test3.ts");
|
|
142
|
+
|
|
143
|
+
// Wait for debounce + idle check
|
|
144
|
+
await new Promise((resolve) => setTimeout(resolve, 400));
|
|
145
|
+
|
|
146
|
+
// Should only restart once despite multiple changes
|
|
147
|
+
expect(onRestartMock).toHaveBeenCalledTimes(1);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("should stop watching when stop() is called", () => {
|
|
151
|
+
restartManager = new RestartManager({
|
|
152
|
+
watchPaths: [testDir],
|
|
153
|
+
isIdle: isIdleMock,
|
|
154
|
+
onRestart: onRestartMock,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
restartManager.start();
|
|
158
|
+
restartManager.stop();
|
|
159
|
+
|
|
160
|
+
// If no error is thrown, the test passes
|
|
161
|
+
expect(true).toBe(true);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { watch, type FSWatcher } from "fs";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* RestartManager watches for file changes and queues a restart
|
|
6
|
+
* that only executes when the bot is idle (no active tasks).
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const restartManager = new RestartManager({
|
|
10
|
+
* watchPaths: ['bot/**\/*.ts', 'src/**\/*.ts'],
|
|
11
|
+
* isIdle: () => TaskInputFlows.size === 0,
|
|
12
|
+
* onRestart: () => process.exit(0),
|
|
13
|
+
* idleCheckInterval: 5000,
|
|
14
|
+
* debounceDelay: 1000
|
|
15
|
+
* });
|
|
16
|
+
* restartManager.start();
|
|
17
|
+
*/
|
|
18
|
+
export class RestartManager {
|
|
19
|
+
private restartPending = false;
|
|
20
|
+
private watchers: FSWatcher[] = [];
|
|
21
|
+
private debounceTimer: Timer | null = null;
|
|
22
|
+
private idleCheckTimer: Timer | null = null;
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
private options: {
|
|
26
|
+
/** Paths to watch for changes (can use glob patterns with Bun) */
|
|
27
|
+
watchPaths: string[];
|
|
28
|
+
/** Function that returns true when the bot is idle */
|
|
29
|
+
isIdle: () => boolean;
|
|
30
|
+
/** Callback to execute the restart (typically process.exit(0)) */
|
|
31
|
+
onRestart: () => void;
|
|
32
|
+
/** How often to check if bot is idle (ms) */
|
|
33
|
+
idleCheckInterval?: number;
|
|
34
|
+
/** Debounce delay for file changes (ms) */
|
|
35
|
+
debounceDelay?: number;
|
|
36
|
+
/** Logger function */
|
|
37
|
+
logger?: {
|
|
38
|
+
info: (msg: string, meta?: unknown) => void;
|
|
39
|
+
warn: (msg: string, meta?: unknown) => void;
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
) {
|
|
43
|
+
this.options.idleCheckInterval ??= 5000;
|
|
44
|
+
this.options.debounceDelay ??= 1000;
|
|
45
|
+
this.options.logger ??= {
|
|
46
|
+
info: (msg) => console.log(`[RestartManager] ${msg}`),
|
|
47
|
+
warn: (msg) => console.warn(`[RestartManager] ${msg}`),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Start watching for file changes
|
|
53
|
+
*/
|
|
54
|
+
start() {
|
|
55
|
+
const { watchPaths, logger } = this.options;
|
|
56
|
+
|
|
57
|
+
logger!.info(`Starting file watcher for ${watchPaths.length} paths`);
|
|
58
|
+
|
|
59
|
+
for (const watchPath of watchPaths) {
|
|
60
|
+
try {
|
|
61
|
+
const absolutePath = resolve(watchPath);
|
|
62
|
+
const watcher = watch(absolutePath, { recursive: true }, (eventType, filename) => {
|
|
63
|
+
logger!.info(`File event: ${eventType} on ${filename}`);
|
|
64
|
+
if (filename) {
|
|
65
|
+
this.onFileChange(filename);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
this.watchers.push(watcher);
|
|
70
|
+
logger!.info(`Watching: ${absolutePath}`);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
logger!.warn(`Failed to watch ${watchPath}: ${error}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Stop watching and clear pending restart
|
|
79
|
+
*/
|
|
80
|
+
stop() {
|
|
81
|
+
const { logger } = this.options;
|
|
82
|
+
|
|
83
|
+
for (const watcher of this.watchers) {
|
|
84
|
+
watcher.close();
|
|
85
|
+
}
|
|
86
|
+
this.watchers = [];
|
|
87
|
+
|
|
88
|
+
if (this.debounceTimer) {
|
|
89
|
+
clearTimeout(this.debounceTimer);
|
|
90
|
+
this.debounceTimer = null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (this.idleCheckTimer) {
|
|
94
|
+
clearInterval(this.idleCheckTimer);
|
|
95
|
+
this.idleCheckTimer = null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.restartPending = false;
|
|
99
|
+
logger!.info("File watcher stopped");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Handle file change event
|
|
104
|
+
*/
|
|
105
|
+
private onFileChange(filename: string) {
|
|
106
|
+
const { debounceDelay, logger } = this.options;
|
|
107
|
+
|
|
108
|
+
// Ignore certain files
|
|
109
|
+
if (this.shouldIgnoreFile(filename)) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
logger!.info(`File changed: ${filename}`);
|
|
114
|
+
|
|
115
|
+
// Clear existing debounce timer
|
|
116
|
+
if (this.debounceTimer) {
|
|
117
|
+
clearTimeout(this.debounceTimer);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Debounce file changes
|
|
121
|
+
this.debounceTimer = setTimeout(() => {
|
|
122
|
+
this.queueRestart();
|
|
123
|
+
}, debounceDelay);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Queue a restart to execute when idle
|
|
128
|
+
*/
|
|
129
|
+
private queueRestart() {
|
|
130
|
+
const { logger, isIdle, idleCheckInterval } = this.options;
|
|
131
|
+
|
|
132
|
+
if (this.restartPending) {
|
|
133
|
+
logger!.info("Restart already pending, skipping duplicate queue");
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
this.restartPending = true;
|
|
138
|
+
logger!.warn("⏳ Restart queued - waiting for bot to become idle...");
|
|
139
|
+
|
|
140
|
+
// Check if already idle
|
|
141
|
+
if (isIdle()) {
|
|
142
|
+
this.executeRestart();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Start checking for idle state
|
|
147
|
+
this.idleCheckTimer = setInterval(() => {
|
|
148
|
+
if (isIdle()) {
|
|
149
|
+
this.executeRestart();
|
|
150
|
+
} else {
|
|
151
|
+
logger!.info("Bot is busy, waiting for idle state...");
|
|
152
|
+
}
|
|
153
|
+
}, idleCheckInterval);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Execute the restart
|
|
158
|
+
*/
|
|
159
|
+
private executeRestart() {
|
|
160
|
+
const { logger, onRestart } = this.options;
|
|
161
|
+
|
|
162
|
+
if (this.idleCheckTimer) {
|
|
163
|
+
clearInterval(this.idleCheckTimer);
|
|
164
|
+
this.idleCheckTimer = null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
logger!.warn("🔄 Bot is idle - restarting now!");
|
|
168
|
+
this.stop();
|
|
169
|
+
onRestart();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Check if file should be ignored
|
|
174
|
+
*/
|
|
175
|
+
private shouldIgnoreFile(filename: string): boolean {
|
|
176
|
+
const ignorePatterns = [
|
|
177
|
+
/node_modules/,
|
|
178
|
+
/\.cache/,
|
|
179
|
+
/\.logs/,
|
|
180
|
+
/\.git/,
|
|
181
|
+
/\.nedb/,
|
|
182
|
+
/\.sqlite/,
|
|
183
|
+
/\.log$/,
|
|
184
|
+
/\.md$/, // Ignore markdown files
|
|
185
|
+
/~$/, // Ignore temp files
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
return ignorePatterns.some((pattern) => pattern.test(filename));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach } from "bun:test";
|
|
2
|
+
import Keyv from "keyv";
|
|
3
|
+
|
|
4
|
+
describe("Working Tasks Manager", () => {
|
|
5
|
+
let State: Keyv;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
// Use in-memory store for testing
|
|
9
|
+
State = new Keyv();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// Helper functions (same as in bot/index.ts)
|
|
13
|
+
async function addWorkingTask(event: unknown) {
|
|
14
|
+
const workingTasks = ((await State.get("current-working-tasks")) as
|
|
15
|
+
| { workingMessageEvents: unknown[] }
|
|
16
|
+
| undefined) || { workingMessageEvents: [] };
|
|
17
|
+
const events = workingTasks.workingMessageEvents || [];
|
|
18
|
+
|
|
19
|
+
const exists = events.some(
|
|
20
|
+
(e: unknown) =>
|
|
21
|
+
(e as Record<string, unknown>).ts === (event as Record<string, unknown>).ts &&
|
|
22
|
+
(e as Record<string, unknown>).channel === (event as Record<string, unknown>).channel,
|
|
23
|
+
);
|
|
24
|
+
if (!exists) {
|
|
25
|
+
events.push(event);
|
|
26
|
+
await State.set("current-working-tasks", { workingMessageEvents: events });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function removeWorkingTask(event: unknown) {
|
|
31
|
+
const workingTasks = ((await State.get("current-working-tasks")) as
|
|
32
|
+
| { workingMessageEvents: unknown[] }
|
|
33
|
+
| undefined) || { workingMessageEvents: [] };
|
|
34
|
+
const events = workingTasks.workingMessageEvents || [];
|
|
35
|
+
|
|
36
|
+
const filtered = events.filter(
|
|
37
|
+
(e: unknown) =>
|
|
38
|
+
!(
|
|
39
|
+
(e as Record<string, unknown>).ts === (event as Record<string, unknown>).ts &&
|
|
40
|
+
(e as Record<string, unknown>).channel === (event as Record<string, unknown>).channel
|
|
41
|
+
),
|
|
42
|
+
);
|
|
43
|
+
await State.set("current-working-tasks", { workingMessageEvents: filtered });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
test("should add task to working list", async () => {
|
|
47
|
+
const event = {
|
|
48
|
+
type: "app_mention" as const,
|
|
49
|
+
user: "U123",
|
|
50
|
+
ts: "1234567890.123456",
|
|
51
|
+
text: "test message",
|
|
52
|
+
team: "T123",
|
|
53
|
+
blocks: [],
|
|
54
|
+
channel: "C123",
|
|
55
|
+
event_ts: "1234567890.123456",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
await addWorkingTask(event);
|
|
59
|
+
|
|
60
|
+
const workingTasks = (await State.get("current-working-tasks")) as
|
|
61
|
+
| { workingMessageEvents: Array<Record<string, unknown>> }
|
|
62
|
+
| undefined;
|
|
63
|
+
expect(workingTasks).toBeDefined();
|
|
64
|
+
expect(workingTasks?.workingMessageEvents).toHaveLength(1);
|
|
65
|
+
expect(workingTasks?.workingMessageEvents[0].ts).toBe(event.ts);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("should not add duplicate tasks", async () => {
|
|
69
|
+
const event = {
|
|
70
|
+
type: "app_mention" as const,
|
|
71
|
+
user: "U123",
|
|
72
|
+
ts: "1234567890.123456",
|
|
73
|
+
text: "test message",
|
|
74
|
+
team: "T123",
|
|
75
|
+
blocks: [],
|
|
76
|
+
channel: "C123",
|
|
77
|
+
event_ts: "1234567890.123456",
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
await addWorkingTask(event);
|
|
81
|
+
await addWorkingTask(event); // Add same event again
|
|
82
|
+
|
|
83
|
+
const workingTasks = (await State.get("current-working-tasks")) as
|
|
84
|
+
| { workingMessageEvents: unknown[] }
|
|
85
|
+
| undefined;
|
|
86
|
+
expect(workingTasks?.workingMessageEvents).toHaveLength(1);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("should remove task from working list", async () => {
|
|
90
|
+
const event = {
|
|
91
|
+
type: "app_mention" as const,
|
|
92
|
+
user: "U123",
|
|
93
|
+
ts: "1234567890.123456",
|
|
94
|
+
text: "test message",
|
|
95
|
+
team: "T123",
|
|
96
|
+
blocks: [],
|
|
97
|
+
channel: "C123",
|
|
98
|
+
event_ts: "1234567890.123456",
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
await addWorkingTask(event);
|
|
102
|
+
await removeWorkingTask(event);
|
|
103
|
+
|
|
104
|
+
const workingTasks = (await State.get("current-working-tasks")) as
|
|
105
|
+
| { workingMessageEvents: unknown[] }
|
|
106
|
+
| undefined;
|
|
107
|
+
expect(workingTasks?.workingMessageEvents).toHaveLength(0);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("should handle multiple tasks", async () => {
|
|
111
|
+
const event1 = {
|
|
112
|
+
type: "app_mention" as const,
|
|
113
|
+
user: "U123",
|
|
114
|
+
ts: "1234567890.123456",
|
|
115
|
+
text: "test message 1",
|
|
116
|
+
team: "T123",
|
|
117
|
+
blocks: [],
|
|
118
|
+
channel: "C123",
|
|
119
|
+
event_ts: "1234567890.123456",
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const event2 = {
|
|
123
|
+
type: "app_mention" as const,
|
|
124
|
+
user: "U456",
|
|
125
|
+
ts: "1234567890.654321",
|
|
126
|
+
text: "test message 2",
|
|
127
|
+
team: "T123",
|
|
128
|
+
blocks: [],
|
|
129
|
+
channel: "C456",
|
|
130
|
+
event_ts: "1234567890.654321",
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
await addWorkingTask(event1);
|
|
134
|
+
await addWorkingTask(event2);
|
|
135
|
+
|
|
136
|
+
let workingTasks = (await State.get("current-working-tasks")) as
|
|
137
|
+
| { workingMessageEvents: Array<Record<string, unknown>> }
|
|
138
|
+
| undefined;
|
|
139
|
+
expect(workingTasks?.workingMessageEvents).toHaveLength(2);
|
|
140
|
+
|
|
141
|
+
await removeWorkingTask(event1);
|
|
142
|
+
|
|
143
|
+
workingTasks = (await State.get("current-working-tasks")) as
|
|
144
|
+
| { workingMessageEvents: Array<Record<string, unknown>> }
|
|
145
|
+
| undefined;
|
|
146
|
+
expect(workingTasks?.workingMessageEvents).toHaveLength(1);
|
|
147
|
+
expect(workingTasks?.workingMessageEvents[0].ts).toBe(event2.ts);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("should handle empty state on first access", async () => {
|
|
151
|
+
const workingTasks = await State.get("current-working-tasks");
|
|
152
|
+
expect(workingTasks).toBeUndefined();
|
|
153
|
+
});
|
|
154
|
+
});
|