@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.
Files changed (66) hide show
  1. package/README.md +82 -426
  2. package/docs/API.md +134 -0
  3. package/docs/SETUP.md +64 -29
  4. package/docs/TROUBLESHOOTING.md +28 -0
  5. package/lib/handlers.js +156 -0
  6. package/lib/slack-client.js +11 -3
  7. package/lib/token-store.js +6 -5
  8. package/lib/tools.js +132 -1
  9. package/package.json +15 -8
  10. package/public/index.html +10 -6
  11. package/public/share.html +6 -5
  12. package/scripts/setup-wizard.js +2 -2
  13. package/server.json +8 -2
  14. package/src/server-http.js +16 -1
  15. package/src/server.js +31 -7
  16. package/src/web-server.js +117 -4
  17. package/docs/CLOUDFLARE-BROWSER-TOOLKIT.md +0 -67
  18. package/docs/COMMUNICATION-STYLE.md +0 -66
  19. package/docs/COMPATIBILITY.md +0 -19
  20. package/docs/DEPLOYMENT-MODES.md +0 -55
  21. package/docs/HN-LAUNCH.md +0 -72
  22. package/docs/INDEX.md +0 -41
  23. package/docs/INSTALL-PROOF.md +0 -18
  24. package/docs/LAUNCH-COPY-v3.0.0.md +0 -101
  25. package/docs/LAUNCH-MATRIX.md +0 -22
  26. package/docs/LAUNCH-OPS.md +0 -71
  27. package/docs/RELEASE-HEALTH.md +0 -77
  28. package/docs/SUPPORT-BOUNDARIES.md +0 -49
  29. package/docs/USE_CASE_RECIPES.md +0 -69
  30. package/docs/WEB-API.md +0 -303
  31. package/docs/images/demo-channel-messages.png +0 -0
  32. package/docs/images/demo-channels.png +0 -0
  33. package/docs/images/demo-claude-mobile-360x800.png +0 -0
  34. package/docs/images/demo-claude-mobile-390x844.png +0 -0
  35. package/docs/images/demo-claude-mobile-poster.png +0 -0
  36. package/docs/images/demo-main-mobile-360x800.png +0 -0
  37. package/docs/images/demo-main-mobile-390x844.png +0 -0
  38. package/docs/images/demo-main.png +0 -0
  39. package/docs/images/demo-messages.png +0 -0
  40. package/docs/images/demo-poster.png +0 -0
  41. package/docs/images/demo-sidebar.png +0 -0
  42. package/docs/images/diagram-oauth-comparison.svg +0 -80
  43. package/docs/images/diagram-session-flow.svg +0 -105
  44. package/docs/images/social-preview-v3.png +0 -0
  45. package/docs/images/web-api-mobile-360x800.png +0 -0
  46. package/docs/images/web-api-mobile-390x844.png +0 -0
  47. package/public/demo-claude.html +0 -1974
  48. package/public/demo-video.html +0 -244
  49. package/public/demo.html +0 -1196
  50. package/scripts/build-mobile-demo.js +0 -168
  51. package/scripts/build-release-health-delta.js +0 -201
  52. package/scripts/build-social-preview.js +0 -189
  53. package/scripts/capture-screenshots.js +0 -152
  54. package/scripts/check-owner-attribution.sh +0 -131
  55. package/scripts/check-public-language.sh +0 -26
  56. package/scripts/check-version-parity.js +0 -218
  57. package/scripts/cloudflare-browser-tool.js +0 -237
  58. package/scripts/collect-release-health.js +0 -162
  59. package/scripts/impact-push-v3.js +0 -781
  60. package/scripts/record-demo.js +0 -163
  61. package/scripts/release-preflight.js +0 -247
  62. package/scripts/setup-git-hooks.sh +0 -15
  63. package/scripts/update-github-social-preview.js +0 -208
  64. package/scripts/verify-core.js +0 -159
  65. package/scripts/verify-install-flow.js +0 -193
  66. package/scripts/verify-web.js +0 -273
@@ -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();
@@ -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
- });