pi-roundtable 0.1.0 → 0.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 (239) hide show
  1. package/CHANGELOG.md +148 -0
  2. package/README.md +10 -6
  3. package/README.zh-TW.md +144 -0
  4. package/docs/plugins.md +1448 -66
  5. package/examples/echo-runtime.test.ts +122 -0
  6. package/examples/echo-runtime.ts +107 -0
  7. package/examples/events.test.ts +1 -0
  8. package/examples/events.ts +2 -2
  9. package/examples/fake-surface.test.ts +180 -0
  10. package/examples/fake-surface.ts +109 -0
  11. package/examples/interactions.test.ts +12 -3
  12. package/examples/interactions.ts +19 -19
  13. package/examples/shared-services.test.ts +74 -0
  14. package/examples/shared-services.ts +60 -0
  15. package/examples/study-room.test.ts +69 -0
  16. package/examples/study-room.ts +52 -0
  17. package/examples/support-desk.test.ts +34 -0
  18. package/examples/support-desk.ts +40 -0
  19. package/package.json +11 -3
  20. package/src/cli/checks/database.ts +11 -30
  21. package/src/cli/checks/images.ts +19 -0
  22. package/src/cli/doctor.ts +6 -0
  23. package/src/cli/main.ts +2 -1
  24. package/src/core/agents/agent-claim.ts +42 -16
  25. package/src/core/agents/agent-messages.ts +4 -4
  26. package/src/core/agents/agent-ports.ts +8 -21
  27. package/src/core/agents/agent-prompt.ts +3 -1
  28. package/src/core/agents/agent-store.ts +6 -4
  29. package/src/core/agents/agent-team-fixture.ts +389 -0
  30. package/src/core/agents/agent-team.ts +26 -25
  31. package/src/core/agents/agent-tools.ts +34 -15
  32. package/src/core/agents/avatar-studio.ts +40 -6
  33. package/src/core/agents/fallback-avatar.ts +85 -0
  34. package/src/core/agents/team-editing.ts +49 -10
  35. package/src/core/agents/team-keys.ts +22 -9
  36. package/src/core/agents/team-layout.ts +4 -3
  37. package/src/core/agents/team-lifecycle.ts +28 -2
  38. package/src/core/agents/team-options.ts +9 -5
  39. package/src/core/agents/team-status.ts +4 -4
  40. package/src/core/agents/team-text.ts +8 -6
  41. package/src/core/agents/team-turn-types.ts +4 -4
  42. package/src/core/agents/team-turns.ts +6 -5
  43. package/src/core/builtin/agent-server.ts +187 -121
  44. package/src/core/builtin/discord-admin.ts +41 -0
  45. package/src/core/builtin/discord.ts +65 -40
  46. package/src/core/builtin/modules.ts +67 -58
  47. package/src/core/builtin/session-tool.ts +24 -0
  48. package/src/core/builtin/skills.ts +72 -0
  49. package/src/core/builtin/stores.ts +55 -31
  50. package/src/core/config/config.ts +45 -4
  51. package/src/core/config/schema.ts +6 -0
  52. package/src/core/contract/channels.ts +54 -14
  53. package/src/core/contract/providers.ts +17 -0
  54. package/src/core/contract/runtime.ts +138 -0
  55. package/src/core/contract/services.ts +45 -0
  56. package/src/core/contract/surface.ts +88 -0
  57. package/src/core/db/migrations.ts +118 -3
  58. package/src/core/define-roundtable.ts +70 -46
  59. package/src/core/define.ts +2 -1
  60. package/src/core/discord/agent-commands.ts +39 -14
  61. package/src/core/discord/agent-panel.ts +66 -0
  62. package/src/core/discord/channel-executor.ts +6 -3
  63. package/src/core/discord/channel-operations.ts +14 -11
  64. package/src/core/discord/command-collection.ts +46 -0
  65. package/src/core/{registry/interactions.ts → discord/compose-commands.ts} +5 -5
  66. package/src/core/discord/connection.ts +35 -0
  67. package/src/core/discord/discord-surface.ts +32 -94
  68. package/src/core/discord/dispatch-threads.ts +2 -2
  69. package/src/core/discord/inbound-message.ts +99 -0
  70. package/src/core/discord/interaction-module.ts +57 -1
  71. package/src/core/discord/owner-cards.ts +4 -4
  72. package/src/core/discord/owner-command.ts +44 -11
  73. package/src/core/discord/owner-discord.ts +2 -2
  74. package/src/core/discord/schedule-commands.ts +21 -18
  75. package/src/core/discord/stop-button.ts +44 -1
  76. package/src/core/domain/attachment.ts +12 -8
  77. package/src/core/domain/conversation.ts +3 -4
  78. package/src/core/domain/owner-prompts.ts +5 -5
  79. package/src/core/domain/ports.ts +9 -53
  80. package/src/core/freeze.ts +9 -0
  81. package/src/core/host.ts +255 -82
  82. package/src/core/http/listeners.ts +55 -20
  83. package/src/core/i18n/agent-panel.ts +4 -0
  84. package/src/core/i18n/index.ts +12 -2
  85. package/src/core/i18n/owner.ts +7 -1
  86. package/src/core/i18n/schedules.ts +12 -6
  87. package/src/core/identity.ts +1 -1
  88. package/src/core/judging/effort-judge.ts +25 -3
  89. package/src/core/log.ts +21 -3
  90. package/src/core/models.ts +2 -2
  91. package/src/core/modules/background/background-turns.ts +6 -4
  92. package/src/core/modules/delegation/delegate.ts +3 -2
  93. package/src/core/modules/delegation/delegator.ts +15 -13
  94. package/src/core/modules/delegation/{sol-worker.ts → web-research-worker.ts} +6 -6
  95. package/src/core/modules/discord-admin/discord-admin.ts +6 -6
  96. package/src/core/modules/host-shell/shell-policy.ts +8 -3
  97. package/src/core/modules/memory/owner-memory-store.ts +27 -25
  98. package/src/core/modules/memory/owner-memory.ts +9 -13
  99. package/src/core/modules/schedules/recurrence.ts +5 -3
  100. package/src/core/modules/schedules/schedule-store.ts +11 -11
  101. package/src/core/modules/schedules/schedule-tools.ts +22 -20
  102. package/src/core/modules/schedules/scheduler.ts +7 -2
  103. package/src/core/modules/schedules/schedules.ts +9 -3
  104. package/src/core/modules/skills/skill-registry.ts +3 -2
  105. package/src/core/modules/skills/skill-store.ts +2 -15
  106. package/src/core/modules/skills/skill-tools.ts +10 -4
  107. package/src/core/ops/error-reporter.ts +1 -1
  108. package/src/core/plugin.ts +232 -28
  109. package/src/core/registry/contributions.ts +173 -14
  110. package/src/core/registry/providers.ts +35 -5
  111. package/src/core/registry/services.ts +229 -0
  112. package/src/core/routing/channel-queue.ts +5 -0
  113. package/src/core/routing/channel-router.ts +21 -7
  114. package/src/core/routing/conversation-turns.ts +139 -0
  115. package/src/core/routing/message-text.ts +9 -2
  116. package/src/core/routing/surface-port.ts +39 -0
  117. package/src/core/runtime/conversation-sessions.ts +4 -3
  118. package/src/core/runtime/extensions/confirmation-fixture.ts +11 -0
  119. package/src/core/runtime/extensions/confirmation-gate.ts +11 -9
  120. package/src/core/runtime/mcp.ts +1 -1
  121. package/src/core/runtime/pending-confirmation-store.ts +10 -12
  122. package/src/core/runtime/pi-agent-runtime.ts +9 -6
  123. package/src/core/runtime/prompt-slot.ts +8 -3
  124. package/src/core/runtime/runtime-types.ts +9 -35
  125. package/src/core/runtime/session-factory.ts +21 -5
  126. package/src/core/runtime/text-tools.ts +1 -3
  127. package/src/core/services.ts +238 -90
  128. package/src/core/sessions.ts +3 -2
  129. package/src/core/shared/{profile-tools.ts → active-tools.ts} +2 -2
  130. package/src/core/shared/delegate-tool.ts +4 -3
  131. package/src/core/shared/schedule-tools.ts +22 -13
  132. package/src/core/shared/session-messages.ts +1 -1
  133. package/src/core/speakers.ts +6 -38
  134. package/src/core/testing/database.ts +1 -7
  135. package/src/core/testing/eager-catalog.ts +30 -0
  136. package/src/core/testing/locale.ts +23 -5
  137. package/src/core/testing/modules.ts +105 -41
  138. package/src/core/testing/test-host.ts +279 -0
  139. package/src/core/testing/tool-set.ts +64 -0
  140. package/src/core/time.ts +6 -1
  141. package/src/core/tool-set.snapshot.json +288 -0
  142. package/src/core/tool-tiers.ts +17 -57
  143. package/src/discord/index.ts +64 -0
  144. package/src/index.ts +181 -10
  145. package/src/kit/channels.ts +14 -0
  146. package/src/kit/domain.ts +18 -0
  147. package/src/kit/holds.ts +4 -0
  148. package/src/kit/index.ts +129 -0
  149. package/src/kit/judging.ts +10 -0
  150. package/src/kit/memory.ts +3 -0
  151. package/src/kit/mirror.ts +19 -0
  152. package/src/kit/presentation.ts +7 -0
  153. package/src/kit/shell.ts +7 -0
  154. package/src/kit/skills.ts +8 -0
  155. package/src/kit/support.ts +21 -0
  156. package/src/kit/threads.ts +7 -0
  157. package/src/kit/tools.ts +14 -0
  158. package/src/kit/worker.ts +16 -0
  159. package/src/testing.ts +394 -34
  160. package/examples/guide.test.ts +0 -52
  161. package/src/cli/add-plugin.test.ts +0 -107
  162. package/src/cli/checks/basic.test.ts +0 -276
  163. package/src/cli/checks/database.test.ts +0 -117
  164. package/src/cli/checks/discord.test.ts +0 -200
  165. package/src/cli/cli.test.ts +0 -122
  166. package/src/cli/config-edit.test.ts +0 -130
  167. package/src/cli/doctor.test.ts +0 -184
  168. package/src/cli/init.test.ts +0 -127
  169. package/src/cli/size.test.ts +0 -9
  170. package/src/cli/templates.test.ts +0 -129
  171. package/src/cli/testing/fixtures.ts +0 -106
  172. package/src/core/agents/agent-claim.test.ts +0 -142
  173. package/src/core/agents/agent-dashboard.test.ts +0 -120
  174. package/src/core/agents/agent-guild.test.ts +0 -124
  175. package/src/core/agents/agent-store.test.ts +0 -205
  176. package/src/core/agents/avatar-studio.test.ts +0 -88
  177. package/src/core/agents/group-round.test.ts +0 -143
  178. package/src/core/agents/owner-identity.test.ts +0 -172
  179. package/src/core/agents/team-turns.test.ts +0 -278
  180. package/src/core/attachments/attachments.test.ts +0 -85
  181. package/src/core/boundary.test.ts +0 -45
  182. package/src/core/builtin/modules.test.ts +0 -127
  183. package/src/core/config/config.test.ts +0 -135
  184. package/src/core/contract/discord.ts +0 -33
  185. package/src/core/db/migrations.test.ts +0 -237
  186. package/src/core/define-roundtable.test.ts +0 -134
  187. package/src/core/define.test.ts +0 -144
  188. package/src/core/discord/agent-commands.test.ts +0 -58
  189. package/src/core/discord/agent-discord.test.ts +0 -70
  190. package/src/core/discord/dispatch-thread-host.test.ts +0 -114
  191. package/src/core/discord/dispatch-threads.test.ts +0 -94
  192. package/src/core/discord/owner-cards.test.ts +0 -435
  193. package/src/core/discord/owner-discord.test.ts +0 -325
  194. package/src/core/domain/expression.ts +0 -21
  195. package/src/core/domain/profile.ts +0 -32
  196. package/src/core/drain.test.ts +0 -46
  197. package/src/core/events.test.ts +0 -81
  198. package/src/core/holds.test.ts +0 -54
  199. package/src/core/host.test.ts +0 -536
  200. package/src/core/http/listeners.test.ts +0 -164
  201. package/src/core/i18n/i18n.test.ts +0 -139
  202. package/src/core/identity.test.ts +0 -18
  203. package/src/core/judging/effort-judge.test.ts +0 -112
  204. package/src/core/judging/model-judge.test.ts +0 -126
  205. package/src/core/modules/delegation/delegator.test.ts +0 -170
  206. package/src/core/modules/memory/owner-memory-store.test.ts +0 -244
  207. package/src/core/modules/schedules/schedule.test.ts +0 -358
  208. package/src/core/modules/skills/skill-kind.test.ts +0 -55
  209. package/src/core/modules/skills/skill-registry.test.ts +0 -376
  210. package/src/core/ops/error-reporter.test.ts +0 -283
  211. package/src/core/presentation/card-cadence.ts +0 -41
  212. package/src/core/presentation/presentation.test.ts +0 -166
  213. package/src/core/public-entry.test.ts +0 -59
  214. package/src/core/registry/contributions.test.ts +0 -215
  215. package/src/core/registry/interactions.test.ts +0 -87
  216. package/src/core/registry/providers.test.ts +0 -103
  217. package/src/core/routing/channel-queue.test.ts +0 -31
  218. package/src/core/routing/channel-router.test.ts +0 -326
  219. package/src/core/routing/conversation-kind.ts +0 -14
  220. package/src/core/routing/settle-turn.test.ts +0 -21
  221. package/src/core/runtime/compaction-tiers.test.ts +0 -227
  222. package/src/core/runtime/extensions/ask-user.test.ts +0 -114
  223. package/src/core/runtime/extensions/self-compact-guard.test.ts +0 -50
  224. package/src/core/runtime/pending-confirmation-store.test.ts +0 -52
  225. package/src/core/runtime/session-archive.test.ts +0 -20
  226. package/src/core/runtime/steerable-run.test.ts +0 -264
  227. package/src/core/runtime/text-tools.test.ts +0 -110
  228. package/src/core/runtime/turn-answer.test.ts +0 -72
  229. package/src/core/runtime/worker-task.test.ts +0 -82
  230. package/src/core/services.test.ts +0 -28
  231. package/src/core/sessions.test.ts +0 -85
  232. package/src/core/shared/unix-server.ts +0 -18
  233. package/src/core/size.test.ts +0 -9
  234. package/src/core/speakers.test.ts +0 -78
  235. package/src/core/testing/file-size.ts +0 -34
  236. package/src/core/time.test.ts +0 -166
  237. package/src/core/tool-tiers.test.ts +0 -117
  238. package/src/entries.test.ts +0 -112
  239. package/src/testing.test.ts +0 -156
@@ -1,122 +0,0 @@
1
- import { afterEach, expect, test } from "bun:test";
2
- import { existsSync, readFileSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { type CliEnvironment, readPackage, runCli } from "./cli.ts";
5
- import { fakePorts, tempDir, validConfig } from "./testing/fixtures.ts";
6
-
7
- const dirs: ReturnType<typeof tempDir>[] = [];
8
- afterEach(() => {
9
- for (const dir of dirs.splice(0)) dir.done();
10
- });
11
-
12
- function cli(overrides: Partial<CliEnvironment> = {}) {
13
- const dir = tempDir();
14
- dirs.push(dir);
15
- const out: string[] = [];
16
- const err: string[] = [];
17
- const launched: unknown[] = [];
18
- const io: CliEnvironment = {
19
- cwd: dir.path,
20
- env: {},
21
- bun: () => ({ version: "1.3.10", required: ">=1.3.0" }),
22
- version: () => "1.2.3",
23
- ports: fakePorts(),
24
- launch: async (defined) => void launched.push(defined),
25
- out: (line) => out.push(line),
26
- err: (line) => err.push(line),
27
- ...overrides,
28
- };
29
- return { io, out, err, launched, dir };
30
- }
31
-
32
- test("no command prints the usage and exits non-zero; help and --version exit zero", async () => {
33
- const none = cli();
34
- expect(await runCli([], none.io)).toBe(1);
35
- expect(none.out.join("\n")).toContain("roundtable init");
36
- const help = cli();
37
- expect(await runCli(["--help"], help.io)).toBe(0);
38
- const version = cli();
39
- expect(await runCli(["--version"], version.io)).toBe(0);
40
- expect(version.out).toEqual(["1.2.3"]);
41
- });
42
-
43
- test("an unknown command or a wrong argument list prints the problem and exits non-zero", async () => {
44
- for (const argv of [
45
- ["frobnicate"],
46
- ["add"],
47
- ["add", "plugin"],
48
- ["add", "thing", "x"],
49
- ["add", "plugin", "a", "b"],
50
- ["init", "a", "b"],
51
- ["doctor", "extra"],
52
- ["start", "--reachable"],
53
- ]) {
54
- const run = cli();
55
- expect(await runCli(argv, run.io)).toBe(1);
56
- expect(run.err.length).toBeGreaterThan(0);
57
- }
58
- });
59
-
60
- test("init prints what it created and the three next steps, and exits zero; a second run refuses", async () => {
61
- const run = cli();
62
- expect(await runCli(["init", "bot"], run.io)).toBe(0);
63
- const text = run.out.join("\n");
64
- expect(text).toContain("roundtable.config.ts");
65
- expect(text).toContain("1. cd bot && bun install");
66
- expect(text).toContain("3. bunx roundtable start");
67
- expect(existsSync(join(run.dir.path, "bot/plugins/hello.ts"))).toBe(true);
68
- const again = await runCli(["init", "bot"], run.io);
69
- expect(again).toBe(1);
70
- expect(run.err.join("\n")).toContain("already exist");
71
- });
72
-
73
- test("init on an old Bun refuses with the install page and creates nothing", async () => {
74
- const run = cli({ bun: () => ({ version: "1.0.0", required: ">=1.3.0" }) });
75
- expect(await runCli(["init"], run.io)).toBe(1);
76
- expect(run.err.join("\n")).toContain("https://bun.sh/docs/installation");
77
- expect(existsSync(join(run.dir.path, "package.json"))).toBe(false);
78
- });
79
-
80
- test("add plugin exits zero after init and non-zero on a bad name", async () => {
81
- const run = cli();
82
- await runCli(["init"], run.io);
83
- expect(await runCli(["add", "plugin", "notes"], run.io)).toBe(0);
84
- expect(
85
- readFileSync(join(run.dir.path, "roundtable.config.ts"), "utf8"),
86
- ).toContain("plugins: [hello, notes]");
87
- expect(await runCli(["add", "plugin", "Bad_Name"], run.io)).toBe(1);
88
- });
89
-
90
- test("doctor exits non-zero when a check fails and zero when none does", async () => {
91
- const failing = cli({
92
- ports: fakePorts(validConfig, { login: async () => undefined }),
93
- });
94
- expect(await runCli(["doctor"], failing.io)).toBe(1);
95
- expect(failing.out.join("\n")).toContain("✗ model login");
96
- });
97
-
98
- test("start exits non-zero without launching when an offline check fails, and launches otherwise", async () => {
99
- const failing = cli({ ports: fakePorts({}) });
100
- expect(await runCli(["start"], failing.io)).toBe(1);
101
- expect(failing.launched).toEqual([]);
102
- expect(failing.err.join("\n")).toContain("✗ configuration");
103
- const working = cli();
104
- expect(await runCli(["start"], working.io)).toBe(0);
105
- expect(working.launched).toHaveLength(1);
106
- });
107
-
108
- test("readPackage reads the version and Bun range, and refuses a package.json without them", () => {
109
- const dir = tempDir();
110
- dirs.push(dir);
111
- dir.write(
112
- "package.json",
113
- JSON.stringify({ version: "0.1.0", engines: { bun: ">=1.3.0" } }),
114
- );
115
- expect(readPackage(dir.path)).toEqual({ version: "0.1.0", bun: ">=1.3.0" });
116
- dir.write("package.json", JSON.stringify({ name: "private" }));
117
- expect(() => readPackage(dir.path)).toThrow(
118
- "installed pi-roundtable package",
119
- );
120
- dir.write("package.json", "{ nope");
121
- expect(() => readPackage(dir.path)).toThrow("not valid JSON");
122
- });
@@ -1,130 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { addPluginToConfig, ConfigEditError } from "./config-edit.ts";
3
-
4
- const notes = { name: "my-notes", ident: "myNotes" };
5
- const edit = (source: string) => addPluginToConfig(source, notes);
6
- const refusal = (source: string): string => {
7
- try {
8
- edit(source);
9
- } catch (error) {
10
- if (error instanceof ConfigEditError) return error.message;
11
- throw error;
12
- }
13
- return "";
14
- };
15
-
16
- describe("addPluginToConfig", () => {
17
- test("adds the import after the imports and the entry after the last one", () => {
18
- const source = `import type { Config } from "node:fs";
19
- import { hello } from "./plugins/hello.ts";
20
-
21
- export default {
22
- name: "Bot",
23
- plugins: [hello],
24
- } satisfies Config;
25
- `;
26
- expect(edit(source)).toBe(`import type { Config } from "node:fs";
27
- import { hello } from "./plugins/hello.ts";
28
- import { myNotes } from "./plugins/my-notes.ts";
29
-
30
- export default {
31
- name: "Bot",
32
- plugins: [hello, myNotes],
33
- } satisfies Config;
34
- `);
35
- });
36
-
37
- test("puts the import in sorted place among the relative imports", () => {
38
- const source = `import { agents } from "./agents.ts";
39
- import { hello } from "./plugins/hello.ts";
40
- import { zed } from "./plugins/zed.ts";
41
-
42
- export default { plugins: [hello, zed] };
43
- `;
44
- const lines = edit(source).split("\n");
45
- expect(lines.slice(0, 4)).toEqual([
46
- 'import { agents } from "./agents.ts";',
47
- 'import { hello } from "./plugins/hello.ts";',
48
- 'import { myNotes } from "./plugins/my-notes.ts";',
49
- 'import { zed } from "./plugins/zed.ts";',
50
- ]);
51
- });
52
-
53
- test("keeps a multi-line list's layout, with or without a trailing comma", () => {
54
- const bare = `import { a } from "./plugins/a.ts";
55
- export default {
56
- plugins: [
57
- a,
58
- ],
59
- };
60
- `;
61
- expect(edit(bare)).toContain("\t\ta,\n\t\tmyNotes,\n\t],");
62
- const noComma = bare.replace("a,\n\t]", "a\n\t]");
63
- expect(edit(noComma)).toContain("\t\ta,\n\t\tmyNotes\n\t],");
64
- });
65
-
66
- test("fills an empty list and adds the import to a file without imports", () => {
67
- const edited = edit("export default { plugins: [] };\n");
68
- expect(edited).toBe(
69
- 'import { myNotes } from "./plugins/my-notes.ts";\n\nexport default { plugins: [myNotes] };\n',
70
- );
71
- });
72
-
73
- test("finds the list under as, satisfies, parentheses, and a named constant", () => {
74
- for (const source of [
75
- "export default { plugins: [a] } as const;",
76
- "export default ({ plugins: [a] });",
77
- "export default { plugins: [a] as Plugin[] };",
78
- "const config = { plugins: [a] } satisfies X;\nexport default config;",
79
- 'export default { "plugins": [a] };',
80
- ])
81
- expect(edit(source)).toContain("a, myNotes]");
82
- });
83
-
84
- test("leaves comments and every other byte as they were", () => {
85
- const source = `// the bot
86
- import { a } from "./plugins/a.ts";
87
-
88
- export default {
89
- // who
90
- owner: { id: "1" },
91
- plugins: [a /* first */], // done
92
- };
93
- `;
94
- const edited = edit(source);
95
- expect(edited).toContain("// the bot");
96
- expect(edited).toContain("// who");
97
- // The entry goes right after the last element, before anything that follows it on the line.
98
- expect(edited).toContain("plugins: [a, myNotes /* first */], // done");
99
- });
100
-
101
- test("refuses a default export with no plugin list, naming the file and what to add", () => {
102
- for (const source of [
103
- "export default { name: 'x' };",
104
- "export default { plugins: somePlugins };",
105
- "export default makeConfig();",
106
- "export const config = { plugins: [] };",
107
- "export default { other: { plugins: [] } };",
108
- ]) {
109
- const text = refusal(source);
110
- expect(text).toContain("roundtable.config.ts");
111
- expect(text).toContain("cannot find the plugin list");
112
- expect(text).toContain('import { myNotes } from "./plugins/my-notes.ts"');
113
- }
114
- });
115
-
116
- test("refuses a name the file already uses, and a plugin already listed", () => {
117
- expect(
118
- refusal(
119
- 'import { myNotes } from "./plugins/my-notes.ts";\nexport default { plugins: [myNotes] };',
120
- ),
121
- ).toContain("myNotes is already used");
122
- expect(
123
- refusal("const myNotes = 1;\nexport default { plugins: [] };"),
124
- ).toContain("already used");
125
- });
126
-
127
- test("refuses a file that does not parse and says so", () => {
128
- expect(refusal("export default { plugins: [ ")).toContain("does not parse");
129
- });
130
- });
@@ -1,184 +0,0 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import type { DefinedRoundtable } from "../core/define-roundtable.ts";
3
- import type { DatabasePort } from "./checks/database.ts";
4
- import { INVITE_PERMISSIONS } from "./discord-api.ts";
5
- import { buildChecks, type DoctorInputs, doctor } from "./doctor.ts";
6
- import { type Check, formatOutcomes, runChecks } from "./report.ts";
7
- import { start } from "./start.ts";
8
- import {
9
- fakeHttp,
10
- fakePorts,
11
- healthyDiscord,
12
- tempDir,
13
- validConfig,
14
- } from "./testing/fixtures.ts";
15
-
16
- const dirs: ReturnType<typeof tempDir>[] = [];
17
- afterEach(() => {
18
- for (const dir of dirs.splice(0)) dir.done();
19
- });
20
-
21
- const workingDatabase: DatabasePort = { check: async () => {} };
22
- const bun = { version: "1.3.10", required: ">=1.3.0" };
23
-
24
- function inputs(overrides: Partial<DoctorInputs> = {}): DoctorInputs {
25
- const dir = tempDir();
26
- dirs.push(dir);
27
- return {
28
- cwd: dir.path,
29
- env: {},
30
- bun,
31
- ports: fakePorts(),
32
- database: workingDatabase,
33
- http: fakeHttp({
34
- ...healthyDiscord(INVITE_PERMISSIONS),
35
- "/": { status: 200, body: undefined },
36
- }),
37
- reachable: false,
38
- ...overrides,
39
- };
40
- }
41
-
42
- describe("doctor", () => {
43
- test("lists the checks in the order the spec gives", () => {
44
- expect(buildChecks(inputs()).map((check) => check.name)).toEqual([
45
- "Bun",
46
- "environment",
47
- "configuration",
48
- "plugins",
49
- "PostgreSQL",
50
- "Discord token",
51
- "Discord guild",
52
- "Discord intents",
53
- "Discord channel",
54
- "model login",
55
- "public URL",
56
- ]);
57
- });
58
-
59
- test("passes on a healthy setup", async () => {
60
- const report = await doctor(inputs());
61
- expect(report.ok).toBe(true);
62
- expect(
63
- report.outcomes.every(({ result }) => result.status !== "fail"),
64
- ).toBe(true);
65
- });
66
-
67
- test("fails when any check fails, and prints each check as passed or failed with its fix", async () => {
68
- const report = await doctor(
69
- inputs({
70
- ports: fakePorts(validConfig, { login: async () => undefined }),
71
- }),
72
- );
73
- expect(report.ok).toBe(false);
74
- const lines = formatOutcomes(report.outcomes);
75
- expect(lines.filter((line) => line.startsWith("✓"))).not.toHaveLength(0);
76
- const failed = lines.findIndex((line) => line.startsWith("✗ model login"));
77
- expect(failed).toBeGreaterThan(-1);
78
- expect(lines[failed + 1]).toStartWith(" ");
79
- expect(lines[failed + 1]).toContain("API key");
80
- });
81
-
82
- test("a missing credential fails the checks that need it and skips what depends on it, changing nothing", async () => {
83
- const incomplete = {
84
- ...validConfig,
85
- discord: { ...validConfig.discord, token: "" },
86
- };
87
- const http = fakeHttp({});
88
- const report = await doctor(inputs({ ports: fakePorts(incomplete), http }));
89
- const byName = Object.fromEntries(
90
- report.outcomes.map(({ name, result }) => [name, result.status]),
91
- );
92
- expect(byName.configuration).toBe("fail");
93
- expect(byName["Discord token"]).toBe("skipped");
94
- expect(byName["Discord guild"]).toBe("skipped");
95
- expect(byName["Discord channel"]).toBe("skipped");
96
- expect(http.requests).toEqual([]);
97
- expect(report.ok).toBe(false);
98
- });
99
-
100
- test("a check that throws is reported as failed without hiding the ones after it", async () => {
101
- const checks: Check[] = [
102
- {
103
- name: "boom",
104
- offline: true,
105
- run: async () => {
106
- throw new Error("kaput");
107
- },
108
- },
109
- { name: "fine", offline: true, run: async () => ({ status: "ok" }) },
110
- ];
111
- const outcomes = await runChecks(checks);
112
- expect(outcomes.map(({ result }) => result.status)).toEqual(["fail", "ok"]);
113
- expect(formatOutcomes(outcomes).join("\n")).toContain("kaput");
114
- });
115
-
116
- test("asks the public address to answer only with reachable", async () => {
117
- const quiet = inputs();
118
- await doctor(quiet);
119
- expect((quiet.http as ReturnType<typeof fakeHttp>).requests).not.toContain(
120
- "/",
121
- );
122
- const asking = inputs({ reachable: true });
123
- await doctor(asking);
124
- expect((asking.http as ReturnType<typeof fakeHttp>).requests).toContain(
125
- "/",
126
- );
127
- });
128
- });
129
-
130
- describe("start", () => {
131
- const launched = () => {
132
- const seen: DefinedRoundtable[] = [];
133
- return {
134
- seen,
135
- launch: async (defined: DefinedRoundtable) => void seen.push(defined),
136
- };
137
- };
138
-
139
- test("runs only the checks that need no network, then starts the bot", async () => {
140
- const http = fakeHttp({});
141
- const { seen, launch } = launched();
142
- const report = await start({ ...inputs({ http }), launch });
143
- expect(report.started).toBe(true);
144
- expect(seen).toHaveLength(1);
145
- expect(http.requests).toEqual([]);
146
- expect(report.outcomes.map(({ name }) => name)).toEqual([
147
- "Bun",
148
- "environment",
149
- "configuration",
150
- "plugins",
151
- "model login",
152
- "public URL",
153
- ]);
154
- });
155
-
156
- test("a failing check stops it before anything launches, with the message doctor prints", async () => {
157
- const failing = inputs({
158
- ports: fakePorts(validConfig, { login: async () => undefined }),
159
- });
160
- const { seen, launch } = launched();
161
- const started = await start({ ...failing, launch });
162
- expect(started.started).toBe(false);
163
- expect(seen).toEqual([]);
164
- const fromDoctor = (await doctor(failing)).outcomes.find(
165
- ({ name }) => name === "model login",
166
- );
167
- const fromStart = started.outcomes.find(
168
- ({ name }) => name === "model login",
169
- );
170
- expect(fromStart).toEqual(fromDoctor as NonNullable<typeof fromDoctor>);
171
- });
172
-
173
- test("does not touch the network when the configuration is wrong", async () => {
174
- const http = fakeHttp({});
175
- const { seen, launch } = launched();
176
- const report = await start({
177
- ...inputs({ ports: fakePorts({}), http }),
178
- launch,
179
- });
180
- expect(report.started).toBe(false);
181
- expect(seen).toEqual([]);
182
- expect(http.requests).toEqual([]);
183
- });
184
- });
@@ -1,127 +0,0 @@
1
- import { afterEach, expect, test } from "bun:test";
2
- import { existsSync, readdirSync, readFileSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { BUN_INSTALL_PAGE } from "./checks/bun.ts";
5
- import { init } from "./init.ts";
6
- import { tempDir } from "./testing/fixtures.ts";
7
-
8
- const dirs: ReturnType<typeof tempDir>[] = [];
9
- afterEach(() => {
10
- for (const dir of dirs.splice(0)) dir.done();
11
- });
12
- const temp = () => {
13
- const dir = tempDir();
14
- dirs.push(dir);
15
- return dir;
16
- };
17
-
18
- const bun = { version: "1.3.10", required: ">=1.3.0" };
19
-
20
- const everything = (dir: string): string[] =>
21
- readdirSync(dir, { recursive: true, withFileTypes: true })
22
- .filter((entry) => entry.isFile())
23
- .map((entry) => join(entry.parentPath, entry.name).slice(dir.length + 1))
24
- .sort();
25
-
26
- test("init writes the skeleton and the hello plugin, pins the version, and prints three next steps", () => {
27
- const dir = temp();
28
- const report = init({ cwd: dir.path, dir: "bot", bun, version: "1.2.3" });
29
- expect(report.ok).toBe(true);
30
- expect(everything(join(dir.path, "bot"))).toEqual(
31
- [
32
- ".env.example",
33
- ".gitignore",
34
- "README.md",
35
- "agents.ts",
36
- "biome.json",
37
- "docker-compose.yml",
38
- "package.json",
39
- "persona/shared.md",
40
- "plugins/hello.test.ts",
41
- "plugins/hello.ts",
42
- "roundtable.config.ts",
43
- "tsconfig.json",
44
- ].sort(),
45
- );
46
- expect([...report.created].sort()).toEqual(everything(join(dir.path, "bot")));
47
- const manifest = JSON.parse(
48
- readFileSync(join(dir.path, "bot/package.json"), "utf8"),
49
- ) as { name: string; dependencies: Record<string, string> };
50
- expect(manifest.name).toBe("bot");
51
- expect(manifest.dependencies["pi-roundtable"]).toBe("1.2.3");
52
- expect(report.nextSteps).toHaveLength(3);
53
- expect(report.nextSteps[0]).toStartWith("cd bot && bun install");
54
- expect(report.nextSteps[1]).toContain("roundtable doctor");
55
- expect(report.nextSteps[2]).toContain("roundtable start");
56
- const hello = readFileSync(join(dir.path, "bot/plugins/hello.ts"), "utf8");
57
- expect(hello).toContain('name: "hello"');
58
- expect(hello).toContain("hello_greet");
59
- });
60
-
61
- test("init in the current directory needs no cd, and no file keeps a placeholder", () => {
62
- const dir = temp();
63
- const report = init({ cwd: dir.path, bun, version: "1.2.3" });
64
- expect(report.ok).toBe(true);
65
- expect(report.nextSteps[0]).toStartWith("bun install");
66
- for (const path of everything(dir.path))
67
- expect(readFileSync(join(dir.path, path), "utf8")).not.toMatch(
68
- /__[A-Z]+__/,
69
- );
70
- });
71
-
72
- test("init refuses before writing anything when Bun is missing, naming the install page", () => {
73
- const dir = temp();
74
- const report = init({
75
- cwd: dir.path,
76
- bun: { version: undefined, required: ">=1.3.0" },
77
- version: "1.2.3",
78
- });
79
- expect(report.ok).toBe(false);
80
- expect(report.problems.join("\n")).toContain(BUN_INSTALL_PAGE);
81
- expect(everything(dir.path)).toEqual([]);
82
- });
83
-
84
- test("init refuses when Bun is older than engines.bun, naming both versions and the page", () => {
85
- const dir = temp();
86
- const report = init({
87
- cwd: dir.path,
88
- bun: { version: "1.2.9", required: ">=1.3.0" },
89
- version: "1.2.3",
90
- });
91
- expect(report.ok).toBe(false);
92
- const text = report.problems.join("\n");
93
- expect(text).toContain("1.2.9");
94
- expect(text).toContain(">=1.3.0");
95
- expect(text).toContain(BUN_INSTALL_PAGE);
96
- expect(everything(dir.path)).toEqual([]);
97
- });
98
-
99
- test("init refuses, listing every file that exists, and writes nothing", () => {
100
- const dir = temp();
101
- dir.write("roundtable.config.ts", "// mine");
102
- dir.write("plugins/hello.ts", "// mine");
103
- const before = everything(dir.path);
104
- const report = init({ cwd: dir.path, bun, version: "1.2.3" });
105
- expect(report.ok).toBe(false);
106
- const text = report.problems.join("\n");
107
- expect(text).toContain("roundtable.config.ts");
108
- expect(text).toContain("plugins/hello.ts");
109
- expect(text).not.toContain("package.json");
110
- expect(everything(dir.path)).toEqual(before);
111
- expect(readFileSync(join(dir.path, "roundtable.config.ts"), "utf8")).toBe(
112
- "// mine",
113
- );
114
- expect(existsSync(join(dir.path, "package.json"))).toBe(false);
115
- });
116
-
117
- test("the generated configuration reads credentials from the environment and holds none", () => {
118
- const dir = temp();
119
- init({ cwd: dir.path, bun, version: "1.2.3" });
120
- const config = readFileSync(join(dir.path, "roundtable.config.ts"), "utf8");
121
- expect(config).toContain("process.env");
122
- for (const key of ["DISCORD_TOKEN", "DATABASE_URL", "OWNER_ID", "MODEL"])
123
- expect(config).toContain(`env("${key}")`);
124
- expect(config).not.toMatch(/\d{15,}/);
125
- expect(config).not.toMatch(/token:\s*"/);
126
- expect(readFileSync(join(dir.path, ".gitignore"), "utf8")).toContain(".env");
127
- });
@@ -1,9 +0,0 @@
1
- import { expect, test } from "bun:test";
2
- import { resolve } from "node:path";
3
- import { oversizedFiles } from "../core/testing/file-size.ts";
4
-
5
- test("no file of the command line is over the size limit", () => {
6
- expect(
7
- oversizedFiles(resolve(import.meta.dir, "../.."), ["src/cli"]),
8
- ).toEqual([]);
9
- });