@jtalk22/slack-mcp 3.1.0 → 3.2.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 +82 -426
- package/docs/API.md +134 -0
- package/docs/SETUP.md +64 -29
- package/docs/TROUBLESHOOTING.md +28 -0
- package/lib/handlers.js +156 -0
- package/lib/slack-client.js +11 -3
- package/lib/token-store.js +6 -5
- package/lib/tools.js +132 -1
- package/package.json +15 -8
- package/public/index.html +10 -6
- package/public/share.html +6 -5
- package/scripts/setup-wizard.js +2 -2
- package/server.json +8 -2
- package/src/server-http.js +16 -1
- package/src/server.js +31 -7
- package/src/web-server.js +117 -4
- package/docs/CLOUDFLARE-BROWSER-TOOLKIT.md +0 -67
- package/docs/COMMUNICATION-STYLE.md +0 -66
- package/docs/COMPATIBILITY.md +0 -19
- package/docs/DEPLOYMENT-MODES.md +0 -55
- package/docs/HN-LAUNCH.md +0 -72
- package/docs/INDEX.md +0 -41
- package/docs/INSTALL-PROOF.md +0 -18
- package/docs/LAUNCH-COPY-v3.0.0.md +0 -101
- package/docs/LAUNCH-MATRIX.md +0 -22
- package/docs/LAUNCH-OPS.md +0 -71
- package/docs/RELEASE-HEALTH.md +0 -77
- package/docs/SUPPORT-BOUNDARIES.md +0 -49
- package/docs/USE_CASE_RECIPES.md +0 -69
- package/docs/WEB-API.md +0 -303
- package/docs/images/demo-channel-messages.png +0 -0
- package/docs/images/demo-channels.png +0 -0
- package/docs/images/demo-claude-mobile-360x800.png +0 -0
- package/docs/images/demo-claude-mobile-390x844.png +0 -0
- package/docs/images/demo-claude-mobile-poster.png +0 -0
- package/docs/images/demo-main-mobile-360x800.png +0 -0
- package/docs/images/demo-main-mobile-390x844.png +0 -0
- package/docs/images/demo-main.png +0 -0
- package/docs/images/demo-messages.png +0 -0
- package/docs/images/demo-poster.png +0 -0
- package/docs/images/demo-sidebar.png +0 -0
- package/docs/images/diagram-oauth-comparison.svg +0 -80
- package/docs/images/diagram-session-flow.svg +0 -105
- package/docs/images/social-preview-v3.png +0 -0
- package/docs/images/web-api-mobile-360x800.png +0 -0
- package/docs/images/web-api-mobile-390x844.png +0 -0
- package/public/demo-claude.html +0 -1974
- package/public/demo-video.html +0 -244
- package/public/demo.html +0 -1196
- package/scripts/build-mobile-demo.js +0 -168
- package/scripts/build-release-health-delta.js +0 -201
- package/scripts/build-social-preview.js +0 -189
- package/scripts/capture-screenshots.js +0 -152
- package/scripts/check-owner-attribution.sh +0 -131
- package/scripts/check-public-language.sh +0 -26
- package/scripts/check-version-parity.js +0 -218
- package/scripts/cloudflare-browser-tool.js +0 -237
- package/scripts/collect-release-health.js +0 -162
- package/scripts/impact-push-v3.js +0 -781
- package/scripts/record-demo.js +0 -163
- package/scripts/release-preflight.js +0 -247
- package/scripts/setup-git-hooks.sh +0 -15
- package/scripts/update-github-social-preview.js +0 -208
- package/scripts/verify-core.js +0 -159
- package/scripts/verify-install-flow.js +0 -193
- package/scripts/verify-web.js +0 -273
package/scripts/verify-core.js
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Core Stability Verification Script
|
|
4
|
-
*
|
|
5
|
-
* Tests:
|
|
6
|
-
* 1. Atomic write - no .tmp artifacts remain after write
|
|
7
|
-
* 2. Server exits cleanly (unref timer doesn't cause zombie)
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { writeFileSync, readFileSync, existsSync, renameSync, unlinkSync, readdirSync } from "fs";
|
|
11
|
-
import { spawn } from "child_process";
|
|
12
|
-
import { homedir } from "os";
|
|
13
|
-
import { join, dirname } from "path";
|
|
14
|
-
import { fileURLToPath } from "url";
|
|
15
|
-
|
|
16
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
-
const TEST_DIR = join(homedir(), ".slack-mcp-test");
|
|
18
|
-
const TEST_FILE = join(TEST_DIR, "test-atomic.json");
|
|
19
|
-
|
|
20
|
-
// ============ Test 1: Atomic Write ============
|
|
21
|
-
|
|
22
|
-
function atomicWriteSync(filePath, content) {
|
|
23
|
-
const tempPath = `${filePath}.${process.pid}.tmp`;
|
|
24
|
-
try {
|
|
25
|
-
writeFileSync(tempPath, content);
|
|
26
|
-
renameSync(tempPath, filePath);
|
|
27
|
-
} catch (e) {
|
|
28
|
-
try { unlinkSync(tempPath); } catch {}
|
|
29
|
-
throw e;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function testAtomicWrite() {
|
|
34
|
-
console.log("\n[TEST 1] Atomic Write");
|
|
35
|
-
console.log("─".repeat(40));
|
|
36
|
-
|
|
37
|
-
// Setup - ensure test directory exists
|
|
38
|
-
const { execSync } = await import("child_process");
|
|
39
|
-
try {
|
|
40
|
-
execSync(`mkdir -p "${TEST_DIR}"`);
|
|
41
|
-
} catch {}
|
|
42
|
-
|
|
43
|
-
// Test successful write
|
|
44
|
-
const testData = { test: "data", timestamp: Date.now() };
|
|
45
|
-
atomicWriteSync(TEST_FILE, JSON.stringify(testData, null, 2));
|
|
46
|
-
|
|
47
|
-
// Verify file exists
|
|
48
|
-
if (!existsSync(TEST_FILE)) {
|
|
49
|
-
console.log(" FAIL: File was not created");
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Verify content
|
|
54
|
-
const readBack = JSON.parse(readFileSync(TEST_FILE, "utf-8"));
|
|
55
|
-
if (readBack.test !== "data") {
|
|
56
|
-
console.log(" FAIL: Content mismatch");
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Check for .tmp artifacts in test dir
|
|
61
|
-
const files = readdirSync(TEST_DIR);
|
|
62
|
-
const tmpFiles = files.filter(f => f.endsWith(".tmp"));
|
|
63
|
-
if (tmpFiles.length > 0) {
|
|
64
|
-
console.log(` FAIL: Found .tmp artifacts: ${tmpFiles.join(", ")}`);
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Cleanup
|
|
69
|
-
try { unlinkSync(TEST_FILE); } catch {}
|
|
70
|
-
|
|
71
|
-
console.log(" PASS: Atomic write completed, no .tmp artifacts");
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// ============ Test 2: Server Exit (No Zombie) ============
|
|
76
|
-
|
|
77
|
-
async function testServerExit() {
|
|
78
|
-
console.log("\n[TEST 2] Server Clean Exit (No Zombie)");
|
|
79
|
-
console.log("─".repeat(40));
|
|
80
|
-
|
|
81
|
-
const serverPath = join(__dirname, "../src/server.js");
|
|
82
|
-
|
|
83
|
-
return new Promise((resolve) => {
|
|
84
|
-
const timeout = 5000; // 5 second timeout
|
|
85
|
-
let exitCode = null;
|
|
86
|
-
let timedOut = false;
|
|
87
|
-
|
|
88
|
-
// Spawn server process
|
|
89
|
-
const proc = spawn("node", [serverPath], {
|
|
90
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
91
|
-
env: { ...process.env, SLACK_TOKEN: "test", SLACK_COOKIE: "test" }
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
// Set timeout - if process doesn't exit after stdin closes, it's a zombie
|
|
95
|
-
const timer = setTimeout(() => {
|
|
96
|
-
timedOut = true;
|
|
97
|
-
console.log(" FAIL: Server did not exit within 5s (zombie process detected)");
|
|
98
|
-
proc.kill("SIGKILL");
|
|
99
|
-
resolve(false);
|
|
100
|
-
}, timeout);
|
|
101
|
-
|
|
102
|
-
proc.on("exit", (code) => {
|
|
103
|
-
exitCode = code;
|
|
104
|
-
clearTimeout(timer);
|
|
105
|
-
if (!timedOut) {
|
|
106
|
-
console.log(` PASS: Server exited cleanly (code: ${code})`);
|
|
107
|
-
resolve(true);
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
proc.on("error", (err) => {
|
|
112
|
-
clearTimeout(timer);
|
|
113
|
-
console.log(` INFO: Server spawn error (expected if no SDK): ${err.message}`);
|
|
114
|
-
// This is OK - we're testing exit behavior, not full functionality
|
|
115
|
-
resolve(true);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
// Close stdin immediately to simulate MCP client disconnect
|
|
119
|
-
proc.stdin.end();
|
|
120
|
-
|
|
121
|
-
// Give it a moment then send SIGTERM
|
|
122
|
-
setTimeout(() => {
|
|
123
|
-
if (exitCode === null && !timedOut) {
|
|
124
|
-
proc.kill("SIGTERM");
|
|
125
|
-
}
|
|
126
|
-
}, 1000);
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// ============ Main ============
|
|
131
|
-
|
|
132
|
-
async function main() {
|
|
133
|
-
console.log("╔════════════════════════════════════════╗");
|
|
134
|
-
console.log("║ Core Stability Verification Tests ║");
|
|
135
|
-
console.log("╚════════════════════════════════════════╝");
|
|
136
|
-
|
|
137
|
-
const results = [];
|
|
138
|
-
|
|
139
|
-
results.push(await testAtomicWrite());
|
|
140
|
-
results.push(await testServerExit());
|
|
141
|
-
|
|
142
|
-
console.log("\n" + "═".repeat(40));
|
|
143
|
-
const passed = results.filter(r => r).length;
|
|
144
|
-
const total = results.length;
|
|
145
|
-
|
|
146
|
-
if (passed === total) {
|
|
147
|
-
console.log(`\n✓ ALL TESTS PASSED (${passed}/${total})`);
|
|
148
|
-
console.log("\nCore stability features verified");
|
|
149
|
-
process.exit(0);
|
|
150
|
-
} else {
|
|
151
|
-
console.log(`\n✗ TESTS FAILED (${passed}/${total})`);
|
|
152
|
-
process.exit(1);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
main().catch(e => {
|
|
157
|
-
console.error("Test error:", e);
|
|
158
|
-
process.exit(1);
|
|
159
|
-
});
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { spawnSync } from "node:child_process";
|
|
4
|
-
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
5
|
-
import { tmpdir } from "node:os";
|
|
6
|
-
import { dirname, join } from "node:path";
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
8
|
-
|
|
9
|
-
const PKG = "@jtalk22/slack-mcp";
|
|
10
|
-
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
-
const strictPublished = process.argv.includes("--strict-published");
|
|
12
|
-
const PUBLISHED_SPEC = `${PKG}@latest`;
|
|
13
|
-
const localVersion = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8")).version;
|
|
14
|
-
|
|
15
|
-
function runNpx(args, options = {}) {
|
|
16
|
-
const cmdArgs = ["-y", PUBLISHED_SPEC, ...args];
|
|
17
|
-
const result = spawnSync("npx", cmdArgs, {
|
|
18
|
-
cwd: options.cwd,
|
|
19
|
-
env: options.env,
|
|
20
|
-
encoding: "utf8",
|
|
21
|
-
timeout: 120000,
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
return {
|
|
25
|
-
args: cmdArgs.join(" "),
|
|
26
|
-
status: result.status,
|
|
27
|
-
stdout: (result.stdout || "").trim(),
|
|
28
|
-
stderr: (result.stderr || "").trim(),
|
|
29
|
-
error: result.error,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function assert(condition, message, details = "") {
|
|
34
|
-
if (!condition) {
|
|
35
|
-
const suffix = details ? `\n${details}` : "";
|
|
36
|
-
throw new Error(`${message}${suffix}`);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function printResult(label, result) {
|
|
41
|
-
console.log(`\n[${label}] npx ${result.args}`);
|
|
42
|
-
console.log(`exit=${result.status}`);
|
|
43
|
-
if (result.stdout) {
|
|
44
|
-
console.log("stdout:");
|
|
45
|
-
console.log(result.stdout);
|
|
46
|
-
}
|
|
47
|
-
if (result.stderr) {
|
|
48
|
-
console.log("stderr:");
|
|
49
|
-
console.log(result.stderr);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function runLocalSetupStatus(options = {}) {
|
|
54
|
-
const result = spawnSync("node", [join(repoRoot, "scripts/setup-wizard.js"), "--status"], {
|
|
55
|
-
cwd: repoRoot,
|
|
56
|
-
env: options.env,
|
|
57
|
-
encoding: "utf8",
|
|
58
|
-
timeout: 120000,
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
return {
|
|
62
|
-
args: "node scripts/setup-wizard.js --status",
|
|
63
|
-
status: result.status,
|
|
64
|
-
stdout: (result.stdout || "").trim(),
|
|
65
|
-
stderr: (result.stderr || "").trim(),
|
|
66
|
-
error: result.error,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function runLocalDoctor(options = {}) {
|
|
71
|
-
const result = spawnSync("node", [join(repoRoot, "scripts/setup-wizard.js"), "--doctor"], {
|
|
72
|
-
cwd: repoRoot,
|
|
73
|
-
env: options.env,
|
|
74
|
-
encoding: "utf8",
|
|
75
|
-
timeout: 120000,
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
return {
|
|
79
|
-
args: "node scripts/setup-wizard.js --doctor",
|
|
80
|
-
status: result.status,
|
|
81
|
-
stdout: (result.stdout || "").trim(),
|
|
82
|
-
stderr: (result.stderr || "").trim(),
|
|
83
|
-
error: result.error,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function main() {
|
|
88
|
-
const testHome = mkdtempSync(join(tmpdir(), "slack-mcp-install-check-"));
|
|
89
|
-
|
|
90
|
-
// Force a clean environment so --status reflects missing credentials.
|
|
91
|
-
const env = { ...process.env, HOME: testHome, USERPROFILE: testHome };
|
|
92
|
-
delete env.SLACK_TOKEN;
|
|
93
|
-
delete env.SLACK_COOKIE;
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
const versionResult = runNpx(["--version"], { cwd: testHome, env });
|
|
97
|
-
printResult("version", versionResult);
|
|
98
|
-
assert(
|
|
99
|
-
versionResult.status === 0,
|
|
100
|
-
"Expected --version to exit 0",
|
|
101
|
-
versionResult.stderr || versionResult.stdout,
|
|
102
|
-
);
|
|
103
|
-
const publishedMatchesLocal = versionResult.stdout.includes(localVersion);
|
|
104
|
-
if (strictPublished) {
|
|
105
|
-
assert(
|
|
106
|
-
publishedMatchesLocal,
|
|
107
|
-
`Expected published npx version to match local ${localVersion}`,
|
|
108
|
-
versionResult.stdout,
|
|
109
|
-
);
|
|
110
|
-
} else if (!publishedMatchesLocal) {
|
|
111
|
-
console.log(
|
|
112
|
-
`warning: npx resolved ${versionResult.stdout || "unknown"} while local version is ${localVersion}; strict published checks are deferred until publish.`
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const helpResult = runNpx(["--help"], { cwd: testHome, env });
|
|
117
|
-
printResult("help", helpResult);
|
|
118
|
-
assert(
|
|
119
|
-
helpResult.status === 0,
|
|
120
|
-
"Expected --help to exit 0",
|
|
121
|
-
helpResult.stderr || helpResult.stdout,
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
const statusResult = runNpx(["--status"], { cwd: testHome, env });
|
|
125
|
-
printResult("status", statusResult);
|
|
126
|
-
assert(
|
|
127
|
-
statusResult.status !== 0,
|
|
128
|
-
"Expected --status to exit non-zero when credentials are missing",
|
|
129
|
-
statusResult.stderr || statusResult.stdout,
|
|
130
|
-
);
|
|
131
|
-
if (strictPublished) {
|
|
132
|
-
assert(
|
|
133
|
-
!statusResult.stderr.includes("Attempting Chrome auto-extraction"),
|
|
134
|
-
"Expected npx --status to be read-only without auto-extraction side effects",
|
|
135
|
-
statusResult.stderr,
|
|
136
|
-
);
|
|
137
|
-
} else if (statusResult.stderr.includes("Attempting Chrome auto-extraction")) {
|
|
138
|
-
console.log("warning: published npx --status still has extraction side effects; re-run with --strict-published after publish.");
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const localStatusResult = runLocalSetupStatus({ env });
|
|
142
|
-
printResult("local-status", localStatusResult);
|
|
143
|
-
assert(
|
|
144
|
-
localStatusResult.status !== 0,
|
|
145
|
-
"Expected local --status to exit non-zero when credentials are missing",
|
|
146
|
-
localStatusResult.stderr || localStatusResult.stdout,
|
|
147
|
-
);
|
|
148
|
-
assert(
|
|
149
|
-
!localStatusResult.stderr.includes("Attempting Chrome auto-extraction"),
|
|
150
|
-
"Expected local --status to be read-only without auto-extraction side effects",
|
|
151
|
-
localStatusResult.stderr,
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
const localDoctorMissingResult = runLocalDoctor({ env });
|
|
155
|
-
printResult("local-doctor-missing", localDoctorMissingResult);
|
|
156
|
-
assert(
|
|
157
|
-
localDoctorMissingResult.status === 1,
|
|
158
|
-
"Expected local --doctor to exit 1 when credentials are missing",
|
|
159
|
-
localDoctorMissingResult.stderr || localDoctorMissingResult.stdout,
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
const invalidEnv = {
|
|
163
|
-
...env,
|
|
164
|
-
SLACK_TOKEN: "xoxc-invalid-token",
|
|
165
|
-
SLACK_COOKIE: "xoxd-invalid-cookie",
|
|
166
|
-
};
|
|
167
|
-
const localDoctorInvalidResult = runLocalDoctor({ env: invalidEnv });
|
|
168
|
-
printResult("local-doctor-invalid", localDoctorInvalidResult);
|
|
169
|
-
assert(
|
|
170
|
-
localDoctorInvalidResult.status === 2,
|
|
171
|
-
"Expected local --doctor to exit 2 when credentials are invalid",
|
|
172
|
-
localDoctorInvalidResult.stderr || localDoctorInvalidResult.stdout,
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
const runtimeEnv = {
|
|
176
|
-
...invalidEnv,
|
|
177
|
-
SLACK_MCP_AUTH_TEST_URL: "http://127.0.0.1:9/auth.test"
|
|
178
|
-
};
|
|
179
|
-
const localDoctorRuntimeResult = runLocalDoctor({ env: runtimeEnv });
|
|
180
|
-
printResult("local-doctor-runtime", localDoctorRuntimeResult);
|
|
181
|
-
assert(
|
|
182
|
-
localDoctorRuntimeResult.status === 3,
|
|
183
|
-
"Expected local --doctor to exit 3 when runtime connectivity fails",
|
|
184
|
-
localDoctorRuntimeResult.stderr || localDoctorRuntimeResult.stdout,
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
console.log("\nInstall flow verification passed.");
|
|
188
|
-
} finally {
|
|
189
|
-
rmSync(testHome, { recursive: true, force: true });
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
main();
|
package/scripts/verify-web.js
DELETED
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Web UI Verification Script
|
|
4
|
-
*
|
|
5
|
-
* Tests:
|
|
6
|
-
* 1. Server starts and prints Magic Link
|
|
7
|
-
* 2. /demo.html contains "STATIC PREVIEW" banner
|
|
8
|
-
* 3. /?key=... serves the dashboard (index.html)
|
|
9
|
-
* 4. /demo-video.html and /public/demo-video.html media assets are reachable
|
|
10
|
-
* 5. Server shuts down cleanly
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { spawn } from "child_process";
|
|
14
|
-
import { dirname, join } from "path";
|
|
15
|
-
import { fileURLToPath } from "url";
|
|
16
|
-
|
|
17
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
-
const SERVER_PATH = join(__dirname, "../src/web-server.js");
|
|
19
|
-
const PORT = 3456; // Use non-standard port to avoid conflicts
|
|
20
|
-
const TIMEOUT = 15000;
|
|
21
|
-
|
|
22
|
-
let serverProc = null;
|
|
23
|
-
|
|
24
|
-
function log(msg) {
|
|
25
|
-
console.log(` ${msg}`);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function cleanup() {
|
|
29
|
-
if (serverProc) {
|
|
30
|
-
serverProc.kill("SIGTERM");
|
|
31
|
-
serverProc = null;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
process.on("exit", cleanup);
|
|
36
|
-
process.on("SIGINT", () => { cleanup(); process.exit(1); });
|
|
37
|
-
process.on("SIGTERM", () => { cleanup(); process.exit(1); });
|
|
38
|
-
|
|
39
|
-
async function startServer() {
|
|
40
|
-
return new Promise((resolve, reject) => {
|
|
41
|
-
let magicLink = null;
|
|
42
|
-
let apiKey = null;
|
|
43
|
-
let output = "";
|
|
44
|
-
|
|
45
|
-
serverProc = spawn("node", [SERVER_PATH], {
|
|
46
|
-
env: { ...process.env, PORT: String(PORT) },
|
|
47
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
const timeout = setTimeout(() => {
|
|
51
|
-
reject(new Error("Server startup timeout - no magic link detected"));
|
|
52
|
-
}, TIMEOUT);
|
|
53
|
-
|
|
54
|
-
serverProc.stderr.on("data", (data) => {
|
|
55
|
-
const text = data.toString();
|
|
56
|
-
output += text;
|
|
57
|
-
|
|
58
|
-
// Look for magic link pattern
|
|
59
|
-
const match = text.match(/Dashboard:\s*(http:\/\/[^\s]+)/);
|
|
60
|
-
if (match) {
|
|
61
|
-
magicLink = match[1];
|
|
62
|
-
// Extract key from URL
|
|
63
|
-
const keyMatch = magicLink.match(/[?&]key=([^&\s]+)/);
|
|
64
|
-
if (keyMatch) {
|
|
65
|
-
apiKey = keyMatch[1];
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Server is ready when we see the full banner
|
|
70
|
-
if (output.includes("Dashboard:") && output.includes("API Key:")) {
|
|
71
|
-
clearTimeout(timeout);
|
|
72
|
-
resolve({ magicLink, apiKey });
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
serverProc.on("error", (err) => {
|
|
77
|
-
clearTimeout(timeout);
|
|
78
|
-
reject(err);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
serverProc.on("exit", (code) => {
|
|
82
|
-
if (code !== null && code !== 0) {
|
|
83
|
-
clearTimeout(timeout);
|
|
84
|
-
reject(new Error(`Server exited with code ${code}`));
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async function testDemoPage() {
|
|
91
|
-
const url = `http://localhost:${PORT}/demo.html`;
|
|
92
|
-
const res = await fetch(url);
|
|
93
|
-
|
|
94
|
-
if (!res.ok) {
|
|
95
|
-
throw new Error(`Failed to fetch demo.html: ${res.status}`);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const html = await res.text();
|
|
99
|
-
|
|
100
|
-
if (!html.includes("STATIC PREVIEW")) {
|
|
101
|
-
throw new Error("demo.html missing 'STATIC PREVIEW' banner");
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (!html.includes("Who is Alex?")) {
|
|
105
|
-
throw new Error("demo.html missing anonymized 'Who is Alex?' scenario");
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return true;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async function testDashboard(apiKey) {
|
|
112
|
-
const url = `http://localhost:${PORT}/?key=${apiKey}`;
|
|
113
|
-
const res = await fetch(url);
|
|
114
|
-
|
|
115
|
-
if (!res.ok) {
|
|
116
|
-
throw new Error(`Failed to fetch dashboard: ${res.status}`);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const html = await res.text();
|
|
120
|
-
|
|
121
|
-
// Should serve index.html (the dashboard)
|
|
122
|
-
if (!html.includes("Slack Web API")) {
|
|
123
|
-
throw new Error("Dashboard page missing 'Slack Web API' title");
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (!html.includes("authModal")) {
|
|
127
|
-
throw new Error("Dashboard missing auth modal");
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return true;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
async function testApiWithKey(apiKey) {
|
|
134
|
-
// Test that API rejects bad key
|
|
135
|
-
const badRes = await fetch(`http://localhost:${PORT}/health`, {
|
|
136
|
-
headers: { "Authorization": "Bearer bad-key" }
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
if (badRes.status !== 401) {
|
|
140
|
-
throw new Error(`Expected 401 for bad key, got ${badRes.status}`);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return true;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async function testDemoVideoAssets() {
|
|
147
|
-
const demoVideoPaths = ["/demo-video.html", "/public/demo-video.html"];
|
|
148
|
-
const requiredAssetCandidates = [
|
|
149
|
-
[
|
|
150
|
-
"/docs/images/demo-poster.png",
|
|
151
|
-
"https://jtalk22.github.io/slack-mcp-server/docs/images/demo-poster.png",
|
|
152
|
-
],
|
|
153
|
-
[
|
|
154
|
-
"/docs/videos/demo-claude.webm",
|
|
155
|
-
"https://jtalk22.github.io/slack-mcp-server/docs/videos/demo-claude.webm",
|
|
156
|
-
],
|
|
157
|
-
];
|
|
158
|
-
|
|
159
|
-
for (const pagePath of demoVideoPaths) {
|
|
160
|
-
const demoVideoUrl = `http://localhost:${PORT}${pagePath}`;
|
|
161
|
-
const demoVideoRes = await fetch(demoVideoUrl);
|
|
162
|
-
|
|
163
|
-
if (!demoVideoRes.ok) {
|
|
164
|
-
throw new Error(`Failed to fetch ${pagePath}: ${demoVideoRes.status}`);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const demoVideoHtml = await demoVideoRes.text();
|
|
168
|
-
|
|
169
|
-
for (const candidates of requiredAssetCandidates) {
|
|
170
|
-
const matched = candidates.find((candidate) => demoVideoHtml.includes(candidate));
|
|
171
|
-
if (!matched) {
|
|
172
|
-
throw new Error(`${pagePath} missing expected media reference: ${candidates.join(" OR ")}`);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const assetUrl = matched.startsWith("http")
|
|
176
|
-
? matched
|
|
177
|
-
: `http://localhost:${PORT}${matched}`;
|
|
178
|
-
|
|
179
|
-
const assetRes = await fetch(assetUrl);
|
|
180
|
-
if (!assetRes.ok) {
|
|
181
|
-
throw new Error(`Demo media not reachable from ${pagePath}: ${assetUrl} (status ${assetRes.status})`);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return true;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
async function main() {
|
|
190
|
-
console.log("╔════════════════════════════════════════╗");
|
|
191
|
-
console.log("║ Web UI Verification Tests ║");
|
|
192
|
-
console.log("╚════════════════════════════════════════╝");
|
|
193
|
-
|
|
194
|
-
const results = [];
|
|
195
|
-
|
|
196
|
-
try {
|
|
197
|
-
// Test 1: Server starts with magic link
|
|
198
|
-
console.log("\n[TEST 1] Server Startup & Magic Link");
|
|
199
|
-
console.log("─".repeat(40));
|
|
200
|
-
|
|
201
|
-
const { magicLink, apiKey } = await startServer();
|
|
202
|
-
|
|
203
|
-
if (!magicLink) {
|
|
204
|
-
throw new Error("No magic link found");
|
|
205
|
-
}
|
|
206
|
-
if (!apiKey) {
|
|
207
|
-
throw new Error("No API key found in magic link");
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
log(`Magic Link: ${magicLink}`);
|
|
211
|
-
log(`API Key: ${apiKey.substring(0, 20)}...`);
|
|
212
|
-
log("PASS: Server started with magic link");
|
|
213
|
-
results.push(true);
|
|
214
|
-
|
|
215
|
-
// Test 2: Demo page
|
|
216
|
-
console.log("\n[TEST 2] Demo Page (/demo.html)");
|
|
217
|
-
console.log("─".repeat(40));
|
|
218
|
-
|
|
219
|
-
await testDemoPage();
|
|
220
|
-
log("PASS: Demo page serves correctly with STATIC PREVIEW banner");
|
|
221
|
-
results.push(true);
|
|
222
|
-
|
|
223
|
-
// Test 3: Dashboard
|
|
224
|
-
console.log("\n[TEST 3] Dashboard (/?key=...)");
|
|
225
|
-
console.log("─".repeat(40));
|
|
226
|
-
|
|
227
|
-
await testDashboard(apiKey);
|
|
228
|
-
log("PASS: Dashboard serves with auth modal");
|
|
229
|
-
results.push(true);
|
|
230
|
-
|
|
231
|
-
// Test 4: API auth
|
|
232
|
-
console.log("\n[TEST 4] API Authentication");
|
|
233
|
-
console.log("─".repeat(40));
|
|
234
|
-
|
|
235
|
-
await testApiWithKey(apiKey);
|
|
236
|
-
log("PASS: API correctly rejects bad keys");
|
|
237
|
-
results.push(true);
|
|
238
|
-
|
|
239
|
-
// Test 5: Demo video/media paths
|
|
240
|
-
console.log("\n[TEST 5] Demo Video Media Reachability");
|
|
241
|
-
console.log("─".repeat(40));
|
|
242
|
-
|
|
243
|
-
await testDemoVideoAssets();
|
|
244
|
-
log("PASS: demo-video media assets are reachable");
|
|
245
|
-
results.push(true);
|
|
246
|
-
|
|
247
|
-
} catch (err) {
|
|
248
|
-
console.log(` FAIL: ${err.message}`);
|
|
249
|
-
results.push(false);
|
|
250
|
-
} finally {
|
|
251
|
-
cleanup();
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Summary
|
|
255
|
-
console.log("\n" + "═".repeat(40));
|
|
256
|
-
const passed = results.filter(r => r).length;
|
|
257
|
-
const total = results.length;
|
|
258
|
-
|
|
259
|
-
if (passed === total) {
|
|
260
|
-
console.log(`\n✓ ALL TESTS PASSED (${passed}/${total})`);
|
|
261
|
-
console.log("\nWeb UI features verified");
|
|
262
|
-
process.exit(0);
|
|
263
|
-
} else {
|
|
264
|
-
console.log(`\n✗ TESTS FAILED (${passed}/${total})`);
|
|
265
|
-
process.exit(1);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
main().catch(e => {
|
|
270
|
-
console.error("Test error:", e);
|
|
271
|
-
cleanup();
|
|
272
|
-
process.exit(1);
|
|
273
|
-
});
|