heyio 0.42.0 → 1.0.0

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 (100) hide show
  1. package/README.md +40 -52
  2. package/dist/api/auth.js +35 -38
  3. package/dist/api/server.js +157 -1139
  4. package/dist/config.js +49 -32
  5. package/dist/copilot/agents.js +72 -1055
  6. package/dist/copilot/client.js +6 -17
  7. package/dist/copilot/io-scheduler.js +55 -139
  8. package/dist/copilot/model-router.js +100 -72
  9. package/dist/copilot/orchestrator.js +91 -515
  10. package/dist/copilot/scheduler.js +67 -189
  11. package/dist/copilot/skills.js +41 -366
  12. package/dist/copilot/system-message.js +40 -200
  13. package/dist/copilot/tools.js +191 -2042
  14. package/dist/daemon.js +54 -201
  15. package/dist/index.js +15 -133
  16. package/dist/mcp/config.js +23 -31
  17. package/dist/mcp/index.js +2 -3
  18. package/dist/mcp/registry.js +33 -88
  19. package/dist/notify.js +18 -100
  20. package/dist/paths.js +13 -24
  21. package/dist/setup.js +35 -0
  22. package/dist/store/db.js +111 -297
  23. package/dist/store/feed.js +29 -97
  24. package/dist/store/instances.js +56 -121
  25. package/dist/store/schedules.js +21 -73
  26. package/dist/store/squads.js +35 -186
  27. package/dist/store/tasks.js +25 -168
  28. package/dist/telegram/bot.js +20 -312
  29. package/dist/telegram/handlers.js +39 -3
  30. package/dist/watchdog.js +31 -45
  31. package/dist/wiki/fs.js +38 -155
  32. package/dist/wiki/search.js +31 -44
  33. package/package.json +5 -8
  34. package/web-dist/assets/ChatView-EFFiln1H.js +11 -0
  35. package/web-dist/assets/FeedView-bN4NMOL7.js +6 -0
  36. package/web-dist/assets/LoginView-CNtasq3n.js +1 -0
  37. package/web-dist/assets/McpView-C2CHiwsi.js +1 -0
  38. package/web-dist/assets/SchedulesView-CyilLban.js +1 -0
  39. package/web-dist/assets/SettingsView-1wLXKEF4.js +1 -0
  40. package/web-dist/assets/SkillsView-BLsD-0u0.js +1 -0
  41. package/web-dist/assets/SquadDetailView-CsCw2ZLp.js +21 -0
  42. package/web-dist/assets/SquadsView-DQ3vFlyO.js +6 -0
  43. package/web-dist/assets/WikiView-19M3oqnq.js +21 -0
  44. package/web-dist/assets/api-WGvTsXaE.js +1 -0
  45. package/web-dist/assets/index-D7M5O-_l.css +1 -0
  46. package/web-dist/assets/index-DZOS9syn.js +95 -0
  47. package/web-dist/assets/plus-BOvyX1BC.js +6 -0
  48. package/web-dist/assets/trash-2-DHoetkC4.js +6 -0
  49. package/web-dist/favicon.svg +4 -1
  50. package/web-dist/index.html +7 -10
  51. package/dist/api/logout.test.js +0 -129
  52. package/dist/api/mcp.test.js +0 -285
  53. package/dist/api/wiki.test.js +0 -283
  54. package/dist/auth/session-logic.js +0 -79
  55. package/dist/auth/session-logic.test.js +0 -201
  56. package/dist/copilot/auto-complete-instance.test.js +0 -104
  57. package/dist/copilot/cron.js +0 -136
  58. package/dist/copilot/event-summary.js +0 -286
  59. package/dist/copilot/instance-deactivate.test.js +0 -119
  60. package/dist/copilot/model-router.test.js +0 -71
  61. package/dist/copilot/review-backfill.js +0 -57
  62. package/dist/copilot/session-timeout.js +0 -112
  63. package/dist/copilot/session-timeout.test.js +0 -372
  64. package/dist/copilot/skills.test.js +0 -55
  65. package/dist/copilot/universes.js +0 -469
  66. package/dist/instance-watchdog.js +0 -104
  67. package/dist/instance-watchdog.test.js +0 -183
  68. package/dist/mcp/client.js +0 -109
  69. package/dist/mcp/client.test.js +0 -99
  70. package/dist/mcp/config.test.js +0 -49
  71. package/dist/mcp/registry.test.js +0 -79
  72. package/dist/notify.test.js +0 -232
  73. package/dist/store/feed.test.js +0 -279
  74. package/dist/store/instances.test.js +0 -310
  75. package/dist/store/io-schedules.js +0 -63
  76. package/dist/store/notifications.js +0 -79
  77. package/dist/store/notifications.test.js +0 -197
  78. package/dist/store/schedule-runs.js +0 -46
  79. package/dist/store/squads.test.js +0 -405
  80. package/dist/store/tasks.test.js +0 -150
  81. package/dist/store/worktrees.js +0 -83
  82. package/dist/tui/index.js +0 -286
  83. package/dist/update.js +0 -81
  84. package/dist/watchdog.test.js +0 -83
  85. package/dist/wiki/wiki-squad.test.js +0 -54
  86. package/web-dist/assets/AgentActivityView-CedxxE6K.js +0 -1
  87. package/web-dist/assets/ChatView-DMkYQo_V.js +0 -4
  88. package/web-dist/assets/FeedView-BH4q-31V.js +0 -1
  89. package/web-dist/assets/InboxView-BVwVP4EW.js +0 -1
  90. package/web-dist/assets/LoginView-DRPDhnwu.js +0 -1
  91. package/web-dist/assets/McpView-D8yWz-lq.js +0 -1
  92. package/web-dist/assets/SchedulesView-BzzyncGF.js +0 -1
  93. package/web-dist/assets/SettingsTabs.vue_vue_type_script_setup_true_lang-oW3ySu7Y.js +0 -1
  94. package/web-dist/assets/SkillsView-oxpYuhx7.js +0 -1
  95. package/web-dist/assets/SquadsView-CaKUIKlq.js +0 -1
  96. package/web-dist/assets/StatusIndicator.vue_vue_type_script_setup_true_lang-8U15Qp_Q.js +0 -1
  97. package/web-dist/assets/WikiView-C5jXUlfW.js +0 -1
  98. package/web-dist/assets/index-BrWzNw-N.css +0 -10
  99. package/web-dist/assets/index-f67odrrt.js +0 -81
  100. package/web-dist/icons.svg +0 -24
@@ -1,285 +0,0 @@
1
- /**
2
- * Integration tests for MCP API endpoints (#279).
3
- *
4
- * Strategy: spin up a minimal Express server that mounts the same MCP route
5
- * handlers as src/api/server.ts, but pointed at a temp config file via
6
- * setMcpConfigPathForTests(). Tests make real HTTP requests using node:http.
7
- * The /mcp/reload endpoint uses an injectable reload fn to avoid pulling in
8
- * the full orchestrator (which needs DB + Copilot client).
9
- */
10
- import { describe, it, before, after, beforeEach } from "node:test";
11
- import assert from "node:assert/strict";
12
- import { mkdtempSync, rmSync } from "node:fs";
13
- import { tmpdir } from "node:os";
14
- import { join } from "node:path";
15
- import http from "node:http";
16
- import express from "express";
17
- import { setMcpConfigPathForTests, resetMcpConfigPath, loadMcpConfig, saveMcpConfig } from "../mcp/config.js";
18
- // ── Helpers ───────────────────────────────────────────────────────────────────
19
- function req(method, port, path, body) {
20
- return new Promise((resolve, reject) => {
21
- const payload = body !== undefined ? JSON.stringify(body) : undefined;
22
- const options = {
23
- hostname: "127.0.0.1",
24
- port,
25
- path,
26
- method,
27
- headers: {
28
- ...(payload ? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload) } : {}),
29
- },
30
- };
31
- const r = http.request(options, (res) => {
32
- let data = "";
33
- res.on("data", (chunk) => { data += chunk; });
34
- res.on("end", () => {
35
- try {
36
- resolve({ status: res.statusCode ?? 0, body: JSON.parse(data) });
37
- }
38
- catch {
39
- resolve({ status: res.statusCode ?? 0, body: data });
40
- }
41
- });
42
- });
43
- r.on("error", reject);
44
- if (payload)
45
- r.write(payload);
46
- r.end();
47
- });
48
- }
49
- // ── Test server setup ─────────────────────────────────────────────────────────
50
- let server;
51
- let port;
52
- let tmpDir;
53
- let configPath;
54
- function buildMcpApp(reloadFn = async () => { }) {
55
- const app = express();
56
- app.use(express.json());
57
- app.get("/api/mcp/servers", (_req, res) => {
58
- try {
59
- const config = loadMcpConfig();
60
- res.json({ servers: config.servers });
61
- }
62
- catch (e) {
63
- res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
64
- }
65
- });
66
- app.post("/api/mcp/servers", (req, res) => {
67
- const { name, command, args, url, env } = req.body;
68
- if (!name) {
69
- res.status(400).json({ error: "name is required" });
70
- return;
71
- }
72
- if (!command && !url) {
73
- res.status(400).json({ error: "command or url is required" });
74
- return;
75
- }
76
- try {
77
- const config = loadMcpConfig();
78
- if (config.servers.find(s => s.name === name)) {
79
- res.status(409).json({ error: "server already exists" });
80
- return;
81
- }
82
- config.servers.push({ name, command, args, url, env, enabled: true });
83
- saveMcpConfig(config);
84
- res.status(201).json({ ok: true });
85
- }
86
- catch (e) {
87
- res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
88
- }
89
- });
90
- app.delete("/api/mcp/servers/:name", (req, res) => {
91
- try {
92
- const config = loadMcpConfig();
93
- const idx = config.servers.findIndex(s => s.name === req.params.name);
94
- if (idx === -1) {
95
- res.status(404).json({ error: "server not found" });
96
- return;
97
- }
98
- config.servers.splice(idx, 1);
99
- saveMcpConfig(config);
100
- res.json({ ok: true });
101
- }
102
- catch (e) {
103
- res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
104
- }
105
- });
106
- app.patch("/api/mcp/servers/:name/toggle", (req, res) => {
107
- try {
108
- const config = loadMcpConfig();
109
- const srv = config.servers.find(s => s.name === req.params.name);
110
- if (!srv) {
111
- res.status(404).json({ error: "server not found" });
112
- return;
113
- }
114
- srv.enabled = srv.enabled === false ? true : false;
115
- saveMcpConfig(config);
116
- res.json({ ok: true, enabled: srv.enabled });
117
- }
118
- catch (e) {
119
- res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
120
- }
121
- });
122
- app.post("/api/mcp/reload", async (_req, res) => {
123
- try {
124
- await reloadFn();
125
- res.json({ ok: true });
126
- }
127
- catch (err) {
128
- res.status(500).json({ error: err instanceof Error ? err.message : "reload failed" });
129
- }
130
- });
131
- return app;
132
- }
133
- before(async () => {
134
- tmpDir = mkdtempSync(join(tmpdir(), "io-mcp-api-test-"));
135
- configPath = join(tmpDir, "mcp.json");
136
- setMcpConfigPathForTests(configPath);
137
- await new Promise((resolve) => {
138
- server = buildMcpApp().listen(0, "127.0.0.1", () => {
139
- port = server.address().port;
140
- resolve();
141
- });
142
- });
143
- });
144
- after(async () => {
145
- await new Promise((resolve, reject) => server.close((e) => e ? reject(e) : resolve()));
146
- resetMcpConfigPath();
147
- rmSync(tmpDir, { recursive: true, force: true });
148
- });
149
- beforeEach(() => {
150
- // Reset config file between tests
151
- saveMcpConfig({ servers: [] });
152
- });
153
- // ── Tests ─────────────────────────────────────────────────────────────────────
154
- describe("GET /api/mcp/servers", () => {
155
- it("returns empty array when no servers configured", async () => {
156
- const res = await req("GET", port, "/api/mcp/servers");
157
- assert.equal(res.status, 200);
158
- assert.deepEqual(res.body.servers, []);
159
- });
160
- it("returns configured servers", async () => {
161
- saveMcpConfig({ servers: [{ name: "figma", command: "npx", enabled: true }] });
162
- const res = await req("GET", port, "/api/mcp/servers");
163
- assert.equal(res.status, 200);
164
- const { servers } = res.body;
165
- assert.equal(servers.length, 1);
166
- assert.equal(servers[0].name, "figma");
167
- });
168
- });
169
- describe("POST /api/mcp/servers", () => {
170
- it("creates a stdio server successfully", async () => {
171
- const res = await req("POST", port, "/api/mcp/servers", {
172
- name: "github",
173
- command: "npx",
174
- args: ["-y", "@modelcontextprotocol/server-github"],
175
- });
176
- assert.equal(res.status, 201);
177
- assert.equal(res.body.ok, true);
178
- const config = loadMcpConfig();
179
- assert.equal(config.servers.length, 1);
180
- assert.equal(config.servers[0].name, "github");
181
- assert.equal(config.servers[0].command, "npx");
182
- assert.deepEqual(config.servers[0].args, ["-y", "@modelcontextprotocol/server-github"]);
183
- assert.equal(config.servers[0].enabled, true);
184
- });
185
- it("creates an SSE server successfully", async () => {
186
- const res = await req("POST", port, "/api/mcp/servers", {
187
- name: "postgres",
188
- url: "http://localhost:3001/sse",
189
- });
190
- assert.equal(res.status, 201);
191
- const config = loadMcpConfig();
192
- assert.equal(config.servers[0].url, "http://localhost:3001/sse");
193
- });
194
- it("returns 400 when name is missing", async () => {
195
- const res = await req("POST", port, "/api/mcp/servers", { command: "npx" });
196
- assert.equal(res.status, 400);
197
- assert.ok(res.body.error.includes("name"));
198
- });
199
- it("returns 400 when both command and url are missing", async () => {
200
- const res = await req("POST", port, "/api/mcp/servers", { name: "bad-server" });
201
- assert.equal(res.status, 400);
202
- assert.ok(res.body.error.includes("command"));
203
- });
204
- it("returns 409 for duplicate server name", async () => {
205
- saveMcpConfig({ servers: [{ name: "figma", command: "npx" }] });
206
- const res = await req("POST", port, "/api/mcp/servers", { name: "figma", command: "npx" });
207
- assert.equal(res.status, 409);
208
- assert.ok(res.body.error.includes("already exists"));
209
- });
210
- });
211
- describe("DELETE /api/mcp/servers/:name", () => {
212
- it("removes an existing server", async () => {
213
- saveMcpConfig({ servers: [{ name: "figma", command: "npx" }, { name: "postgres", url: "http://localhost/sse" }] });
214
- const res = await req("DELETE", port, "/api/mcp/servers/figma");
215
- assert.equal(res.status, 200);
216
- assert.equal(res.body.ok, true);
217
- const config = loadMcpConfig();
218
- assert.equal(config.servers.length, 1);
219
- assert.equal(config.servers[0].name, "postgres");
220
- });
221
- it("returns 404 for unknown server", async () => {
222
- const res = await req("DELETE", port, "/api/mcp/servers/nonexistent");
223
- assert.equal(res.status, 404);
224
- assert.ok(res.body.error.includes("not found"));
225
- });
226
- });
227
- describe("PATCH /api/mcp/servers/:name/toggle", () => {
228
- it("disables an enabled server", async () => {
229
- saveMcpConfig({ servers: [{ name: "figma", command: "npx", enabled: true }] });
230
- const res = await req("PATCH", port, "/api/mcp/servers/figma/toggle");
231
- assert.equal(res.status, 200);
232
- assert.equal(res.body.enabled, false);
233
- assert.equal(loadMcpConfig().servers[0].enabled, false);
234
- });
235
- it("enables a disabled server", async () => {
236
- saveMcpConfig({ servers: [{ name: "figma", command: "npx", enabled: false }] });
237
- const res = await req("PATCH", port, "/api/mcp/servers/figma/toggle");
238
- assert.equal(res.status, 200);
239
- assert.equal(res.body.enabled, true);
240
- });
241
- it("returns 404 for unknown server", async () => {
242
- const res = await req("PATCH", port, "/api/mcp/servers/nonexistent/toggle");
243
- assert.equal(res.status, 404);
244
- assert.ok(res.body.error.includes("not found"));
245
- });
246
- });
247
- describe("POST /api/mcp/reload", () => {
248
- it("calls reload function and returns ok", async () => {
249
- let reloadCalled = false;
250
- const app = buildMcpApp(async () => { reloadCalled = true; });
251
- await new Promise((resolve, reject) => {
252
- const s = app.listen(0, "127.0.0.1", async () => {
253
- const p = s.address().port;
254
- try {
255
- const res = await req("POST", p, "/api/mcp/reload");
256
- assert.equal(res.status, 200);
257
- assert.equal(res.body.ok, true);
258
- assert.ok(reloadCalled, "reload function should have been called");
259
- }
260
- finally {
261
- s.close(() => resolve());
262
- }
263
- });
264
- s.on("error", reject);
265
- });
266
- });
267
- it("returns 500 when reload throws", async () => {
268
- const app = buildMcpApp(async () => { throw new Error("init failed"); });
269
- await new Promise((resolve, reject) => {
270
- const s = app.listen(0, "127.0.0.1", async () => {
271
- const p = s.address().port;
272
- try {
273
- const res = await req("POST", p, "/api/mcp/reload");
274
- assert.equal(res.status, 500);
275
- assert.ok(res.body.error.includes("init failed"));
276
- }
277
- finally {
278
- s.close(() => resolve());
279
- }
280
- });
281
- s.on("error", reject);
282
- });
283
- });
284
- });
285
- //# sourceMappingURL=mcp.test.js.map
@@ -1,283 +0,0 @@
1
- /**
2
- * Integration tests for Wiki API endpoints (#312).
3
- * Tests POST, PUT, DELETE endpoints for wiki pages.
4
- */
5
- import { describe, it, before, after, afterEach } from "node:test";
6
- import assert from "node:assert/strict";
7
- import http from "node:http";
8
- import express from "express";
9
- import { listPages, readPage, writePage, deletePage, assertPagePath, ensureWikiStructure, } from "../wiki/fs.js";
10
- // ── Helpers ───────────────────────────────────────────────────────────────────
11
- function req(method, port, path, body) {
12
- return new Promise((resolve, reject) => {
13
- const payload = body !== undefined ? JSON.stringify(body) : undefined;
14
- const options = {
15
- hostname: "127.0.0.1",
16
- port,
17
- path,
18
- method,
19
- headers: {
20
- ...(payload ? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload) } : {}),
21
- },
22
- };
23
- const r = http.request(options, (res) => {
24
- let data = "";
25
- res.on("data", (chunk) => { data += chunk; });
26
- res.on("end", () => {
27
- try {
28
- resolve({ status: res.statusCode ?? 0, body: data ? JSON.parse(data) : {} });
29
- }
30
- catch {
31
- resolve({ status: res.statusCode ?? 0, body: data });
32
- }
33
- });
34
- });
35
- r.on("error", reject);
36
- if (payload)
37
- r.write(payload);
38
- r.end();
39
- });
40
- }
41
- // ── Server Setup ──────────────────────────────────────────────────────────────
42
- function createTestServer() {
43
- const app = express();
44
- app.use(express.json());
45
- // Wiki list
46
- app.get("/api/wiki", (_req, res) => {
47
- try {
48
- const pages = listPages();
49
- const result = pages.map((pagePath) => {
50
- const pageContent = readPage(pagePath);
51
- const title = pageContent?.match(/^#\s+(.+)/m)?.[1]?.trim() ?? pagePath;
52
- return { path: pagePath, title };
53
- });
54
- res.json({ pages: result });
55
- }
56
- catch (e) {
57
- res.status(500).json({ error: "Failed to list wiki pages" });
58
- }
59
- });
60
- // Wiki read
61
- app.get("/api/wiki/*path", (req, res) => {
62
- const pagePath = Array.isArray(req.params.path) ? req.params.path.join("/") : req.params.path;
63
- if (!pagePath) {
64
- res.status(400).json({ error: "Missing page path" });
65
- return;
66
- }
67
- const pageContent = readPage(pagePath);
68
- if (pageContent === undefined) {
69
- res.status(404).json({ error: "Page not found" });
70
- return;
71
- }
72
- res.json({ path: pagePath, content: pageContent });
73
- });
74
- // Wiki create
75
- app.post("/api/wiki", (req, res) => {
76
- const { path: pagePath, content } = req.body;
77
- if (!pagePath || typeof pagePath !== "string") {
78
- res.status(400).json({ error: "Missing page path" });
79
- return;
80
- }
81
- if (content === undefined || typeof content !== "string") {
82
- res.status(400).json({ error: "Missing page content" });
83
- return;
84
- }
85
- try {
86
- assertPagePath(pagePath);
87
- }
88
- catch (e) {
89
- res.status(400).json({ error: e.message });
90
- return;
91
- }
92
- if (readPage(pagePath) !== undefined) {
93
- res.status(409).json({ error: "Page already exists" });
94
- return;
95
- }
96
- writePage(pagePath, content);
97
- res.status(201).json({ path: pagePath, content });
98
- });
99
- // Wiki update
100
- app.put("/api/wiki/*path", (req, res) => {
101
- const pagePath = Array.isArray(req.params.path) ? req.params.path.join("/") : req.params.path;
102
- if (!pagePath) {
103
- res.status(400).json({ error: "Missing page path" });
104
- return;
105
- }
106
- const { content } = req.body;
107
- if (content === undefined || typeof content !== "string") {
108
- res.status(400).json({ error: "Missing page content" });
109
- return;
110
- }
111
- try {
112
- assertPagePath(pagePath);
113
- }
114
- catch (e) {
115
- res.status(400).json({ error: e.message });
116
- return;
117
- }
118
- if (readPage(pagePath) === undefined) {
119
- res.status(404).json({ error: "Page not found" });
120
- return;
121
- }
122
- writePage(pagePath, content);
123
- res.json({ path: pagePath, content });
124
- });
125
- // Wiki delete
126
- app.delete("/api/wiki/*path", (req, res) => {
127
- const pagePath = Array.isArray(req.params.path) ? req.params.path.join("/") : req.params.path;
128
- if (!pagePath) {
129
- res.status(400).json({ error: "Missing page path" });
130
- return;
131
- }
132
- try {
133
- assertPagePath(pagePath);
134
- }
135
- catch (e) {
136
- res.status(400).json({ error: e.message });
137
- return;
138
- }
139
- const deleted = deletePage(pagePath);
140
- if (!deleted) {
141
- res.status(404).json({ error: "Page not found" });
142
- return;
143
- }
144
- res.status(204).send();
145
- });
146
- // Wiki categories
147
- app.get("/api/wiki-categories", (_req, res) => {
148
- res.json({ categories: ["preferences", "projects", "people", "general", "squads"] });
149
- });
150
- return app;
151
- }
152
- // ── Tests ─────────────────────────────────────────────────────────────────────
153
- describe("Wiki API Endpoints", () => {
154
- let server;
155
- let port;
156
- const testPagePath = `pages/general/test-wiki-api-${Date.now()}.md`;
157
- before(() => {
158
- ensureWikiStructure();
159
- const app = createTestServer();
160
- server = app.listen(0);
161
- port = server.address().port;
162
- });
163
- after(() => {
164
- server.close();
165
- // Clean up test page if exists
166
- try {
167
- deletePage(testPagePath);
168
- }
169
- catch { /* ignore */ }
170
- });
171
- afterEach(() => {
172
- // Clean up test page after each test
173
- try {
174
- deletePage(testPagePath);
175
- }
176
- catch { /* ignore */ }
177
- });
178
- describe("GET /api/wiki", () => {
179
- it("returns list of wiki pages", async () => {
180
- const { status, body } = await req("GET", port, "/api/wiki");
181
- assert.equal(status, 200);
182
- assert.ok(Array.isArray(body.pages));
183
- });
184
- });
185
- describe("GET /api/wiki-categories", () => {
186
- it("returns available categories", async () => {
187
- const { status, body } = await req("GET", port, "/api/wiki-categories");
188
- assert.equal(status, 200);
189
- const categories = body.categories;
190
- assert.ok(categories.includes("general"));
191
- assert.ok(categories.includes("projects"));
192
- assert.ok(categories.includes("preferences"));
193
- });
194
- });
195
- describe("POST /api/wiki", () => {
196
- it("creates a new wiki page", async () => {
197
- const { status, body } = await req("POST", port, "/api/wiki", {
198
- path: testPagePath,
199
- content: "# Test Page\n\nThis is a test.",
200
- });
201
- assert.equal(status, 201);
202
- assert.equal(body.path, testPagePath);
203
- // Verify it was created
204
- const page = readPage(testPagePath);
205
- assert.ok(page?.includes("# Test Page"));
206
- });
207
- it("returns 409 if page already exists", async () => {
208
- writePage(testPagePath, "# Existing");
209
- const { status, body } = await req("POST", port, "/api/wiki", {
210
- path: testPagePath,
211
- content: "# New Content",
212
- });
213
- assert.equal(status, 409);
214
- assert.equal(body.error, "Page already exists");
215
- });
216
- it("returns 400 for missing path", async () => {
217
- const { status } = await req("POST", port, "/api/wiki", {
218
- content: "# Test",
219
- });
220
- assert.equal(status, 400);
221
- });
222
- it("returns 400 for missing content", async () => {
223
- const { status } = await req("POST", port, "/api/wiki", {
224
- path: testPagePath,
225
- });
226
- assert.equal(status, 400);
227
- });
228
- it("returns 400 for invalid path (not under pages/)", async () => {
229
- const { status, body } = await req("POST", port, "/api/wiki", {
230
- path: "invalid/path.md",
231
- content: "# Test",
232
- });
233
- assert.equal(status, 400);
234
- assert.ok(body.error.includes("pages/"));
235
- });
236
- });
237
- describe("PUT /api/wiki/*path", () => {
238
- it("updates an existing wiki page", async () => {
239
- writePage(testPagePath, "# Original Content");
240
- const { status, body } = await req("PUT", port, `/api/wiki/${testPagePath}`, {
241
- content: "# Updated Content\n\nNew text here.",
242
- });
243
- assert.equal(status, 200);
244
- assert.equal(body.path, testPagePath);
245
- // Verify it was updated
246
- const page = readPage(testPagePath);
247
- assert.ok(page?.includes("Updated Content"));
248
- });
249
- it("returns 404 if page does not exist", async () => {
250
- const { status, body } = await req("PUT", port, `/api/wiki/${testPagePath}`, {
251
- content: "# New Content",
252
- });
253
- assert.equal(status, 404);
254
- assert.equal(body.error, "Page not found");
255
- });
256
- it("returns 400 for missing content", async () => {
257
- writePage(testPagePath, "# Original");
258
- const { status } = await req("PUT", port, `/api/wiki/${testPagePath}`, {});
259
- assert.equal(status, 400);
260
- });
261
- });
262
- describe("DELETE /api/wiki/*path", () => {
263
- it("deletes an existing wiki page", async () => {
264
- writePage(testPagePath, "# To Delete");
265
- const { status } = await req("DELETE", port, `/api/wiki/${testPagePath}`);
266
- assert.equal(status, 204);
267
- // Verify it was deleted
268
- const page = readPage(testPagePath);
269
- assert.equal(page, undefined);
270
- });
271
- it("returns 404 if page does not exist", async () => {
272
- const { status, body } = await req("DELETE", port, `/api/wiki/${testPagePath}`);
273
- assert.equal(status, 404);
274
- assert.equal(body.error, "Page not found");
275
- });
276
- it("returns 400 for invalid path", async () => {
277
- const { status, body } = await req("DELETE", port, "/api/wiki/invalid-path.md");
278
- assert.equal(status, 400);
279
- assert.ok(body.error.includes("pages/"));
280
- });
281
- });
282
- });
283
- //# sourceMappingURL=wiki.test.js.map
@@ -1,79 +0,0 @@
1
- /**
2
- * Pure session-handling logic extracted from web/src/stores/auth.ts.
3
- *
4
- * These functions contain the core behavioral decisions made across four
5
- * consecutive auth patches (#189→#192→#194→#196) that fixed undocumented
6
- * Supabase JS v2 side-effects. They are expressed as framework-agnostic
7
- * functions so they can be unit-tested with the Node test runner without
8
- * requiring Vue or Pinia.
9
- *
10
- * The authoritative implementation lives in web/src/stores/auth.ts.
11
- * Any change to the auth store's session logic MUST be reflected here and
12
- * all tests in session-logic.test.ts must continue to pass.
13
- */
14
- /**
15
- * Mirrors the onAuthStateChange handler in web/src/stores/auth.ts.
16
- *
17
- * Decision: only SIGNED_OUT clears the cached session. All other events that
18
- * fire with a null session (TOKEN_REFRESHED failure, lock timeout, internal
19
- * reconciliation) are intentionally ignored to prevent cache poisoning.
20
- *
21
- * See squad decision 2026-05-16 03:17:52 and issue #193.
22
- */
23
- export function handleAuthStateChange(cachedSession, event, newSession) {
24
- if (newSession) {
25
- // Accept any valid session regardless of event type
26
- return newSession;
27
- }
28
- if (event === "SIGNED_OUT") {
29
- // Only explicit sign-out clears the cache
30
- return null;
31
- }
32
- // All other null-session events: leave the existing cache untouched
33
- return cachedSession;
34
- }
35
- /**
36
- * Mirrors getAccessToken() in web/src/stores/auth.ts.
37
- *
38
- * Decisions:
39
- * - Reads from cached session — NEVER calls getSession() (Supabase JS v2
40
- * side-effect: can fire onAuthStateChange(null) during reconciliation).
41
- * - Proactively refreshes when within 30 seconds of expiry.
42
- * - Falls back to refreshSession() if cache is null and auth is enabled.
43
- * - Returns null when auth is disabled.
44
- *
45
- * See squad decisions 2026-05-16 02:23:38, 2026-05-16 03:16:46 and issues
46
- * #191, #193.
47
- */
48
- export async function getAccessToken(cachedSession, authEnabled, refresh, now = Date.now) {
49
- if (cachedSession?.access_token) {
50
- // Proactively refresh when within 30 seconds of expiry
51
- if (cachedSession.expires_at !== undefined &&
52
- cachedSession.expires_at * 1000 - now() < 30_000) {
53
- try {
54
- const fresh = await refresh();
55
- if (fresh)
56
- return fresh.access_token;
57
- }
58
- catch {
59
- // fall through to cached token
60
- }
61
- }
62
- return cachedSession.access_token;
63
- }
64
- // No cached session — attempt recovery if auth is enabled.
65
- // Handles the edge case where the cache was spuriously cleared but a
66
- // valid refresh token still exists in storage.
67
- if (authEnabled) {
68
- try {
69
- const fresh = await refresh();
70
- if (fresh)
71
- return fresh.access_token;
72
- }
73
- catch {
74
- // fall through to null
75
- }
76
- }
77
- return null;
78
- }
79
- //# sourceMappingURL=session-logic.js.map