@dobby.ai/dobby 0.1.0 → 0.1.2

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 (156) hide show
  1. package/README.md +84 -39
  2. package/dist/src/agent/event-forwarder.js +185 -16
  3. package/dist/src/cli/commands/cron.js +39 -35
  4. package/dist/src/cli/commands/doctor.js +81 -2
  5. package/dist/src/cli/commands/extension.js +3 -1
  6. package/dist/src/cli/commands/init.js +43 -173
  7. package/dist/src/cli/commands/topology.js +38 -14
  8. package/dist/src/cli/program.js +15 -137
  9. package/dist/src/cli/shared/config-io.js +3 -31
  10. package/dist/src/cli/shared/config-mutators.js +33 -9
  11. package/dist/src/cli/shared/configure-sections.js +52 -12
  12. package/dist/src/cli/shared/init-catalog.js +89 -46
  13. package/dist/src/cli/shared/local-extension-specs.js +85 -0
  14. package/dist/src/cli/shared/schema-prompts.js +26 -2
  15. package/dist/src/core/gateway.js +3 -1
  16. package/dist/src/core/routing.js +53 -38
  17. package/dist/src/core/types.js +2 -0
  18. package/dist/src/cron/config.js +2 -2
  19. package/dist/src/cron/service.js +87 -23
  20. package/dist/src/cron/store.js +1 -1
  21. package/dist/src/main.js +0 -0
  22. package/dist/src/shared/dobby-repo.js +40 -0
  23. package/package.json +11 -4
  24. package/.env.example +0 -9
  25. package/AGENTS.md +0 -267
  26. package/ROADMAP.md +0 -34
  27. package/config/cron.example.json +0 -9
  28. package/config/gateway.example.json +0 -128
  29. package/config/models.custom.example.json +0 -27
  30. package/dist/src/agent/tests/event-forwarder.test.js +0 -113
  31. package/dist/src/cli/shared/config-path.js +0 -207
  32. package/dist/src/cli/shared/init-models-file.js +0 -65
  33. package/dist/src/cli/shared/presets.js +0 -86
  34. package/dist/src/cli/tests/config-command.test.js +0 -42
  35. package/dist/src/cli/tests/config-io.test.js +0 -64
  36. package/dist/src/cli/tests/config-mutators.test.js +0 -47
  37. package/dist/src/cli/tests/config-path.test.js +0 -21
  38. package/dist/src/cli/tests/discord-config.test.js +0 -23
  39. package/dist/src/cli/tests/doctor.test.js +0 -107
  40. package/dist/src/cli/tests/init-catalog.test.js +0 -87
  41. package/dist/src/cli/tests/presets.test.js +0 -41
  42. package/dist/src/cli/tests/program-options.test.js +0 -92
  43. package/dist/src/cli/tests/routing-config.test.js +0 -199
  44. package/dist/src/cli/tests/routing-legacy.test.js +0 -191
  45. package/dist/src/core/tests/control-command.test.js +0 -17
  46. package/dist/src/core/tests/gateway-update-strategy.test.js +0 -167
  47. package/dist/src/core/tests/runtime-registry.test.js +0 -116
  48. package/dist/src/core/tests/typing-controller.test.js +0 -103
  49. package/docs/BOXLITE_SANDBOX_FEASIBILITY.md +0 -175
  50. package/docs/CRON_SCHEDULER_DESIGN.md +0 -374
  51. package/docs/DOCKER_SANDBOX_vs_BOXLITE.md +0 -77
  52. package/docs/EXTENSION_SYSTEM_ARCHITECTURE.md +0 -119
  53. package/docs/MVP.md +0 -135
  54. package/docs/RUNBOOK.md +0 -242
  55. package/docs/TEAMWORK_HANDOFF_DESIGN.md +0 -440
  56. package/plugins/connector-discord/dobby.manifest.json +0 -18
  57. package/plugins/connector-discord/index.js +0 -1
  58. package/plugins/connector-discord/package-lock.json +0 -360
  59. package/plugins/connector-discord/package.json +0 -38
  60. package/plugins/connector-discord/src/connector.ts +0 -350
  61. package/plugins/connector-discord/src/contribution.ts +0 -21
  62. package/plugins/connector-discord/src/mapper.ts +0 -102
  63. package/plugins/connector-discord/tsconfig.json +0 -19
  64. package/plugins/connector-feishu/dobby.manifest.json +0 -18
  65. package/plugins/connector-feishu/index.js +0 -1
  66. package/plugins/connector-feishu/package-lock.json +0 -618
  67. package/plugins/connector-feishu/package.json +0 -38
  68. package/plugins/connector-feishu/src/connector.ts +0 -343
  69. package/plugins/connector-feishu/src/contribution.ts +0 -26
  70. package/plugins/connector-feishu/src/mapper.ts +0 -401
  71. package/plugins/connector-feishu/tsconfig.json +0 -19
  72. package/plugins/plugin-sdk/index.d.ts +0 -261
  73. package/plugins/plugin-sdk/index.js +0 -1
  74. package/plugins/plugin-sdk/package-lock.json +0 -12
  75. package/plugins/plugin-sdk/package.json +0 -22
  76. package/plugins/provider-claude/dobby.manifest.json +0 -17
  77. package/plugins/provider-claude/index.js +0 -1
  78. package/plugins/provider-claude/package-lock.json +0 -3398
  79. package/plugins/provider-claude/package.json +0 -39
  80. package/plugins/provider-claude/src/contribution.ts +0 -1018
  81. package/plugins/provider-claude/tsconfig.json +0 -19
  82. package/plugins/provider-claude-cli/dobby.manifest.json +0 -17
  83. package/plugins/provider-claude-cli/index.js +0 -1
  84. package/plugins/provider-claude-cli/package-lock.json +0 -2898
  85. package/plugins/provider-claude-cli/package.json +0 -38
  86. package/plugins/provider-claude-cli/src/contribution.ts +0 -1673
  87. package/plugins/provider-claude-cli/tsconfig.json +0 -19
  88. package/plugins/provider-pi/dobby.manifest.json +0 -17
  89. package/plugins/provider-pi/index.js +0 -1
  90. package/plugins/provider-pi/package-lock.json +0 -3877
  91. package/plugins/provider-pi/package.json +0 -40
  92. package/plugins/provider-pi/src/contribution.ts +0 -476
  93. package/plugins/provider-pi/tsconfig.json +0 -19
  94. package/plugins/sandbox-core/boxlite.js +0 -1
  95. package/plugins/sandbox-core/dobby.manifest.json +0 -17
  96. package/plugins/sandbox-core/docker.js +0 -1
  97. package/plugins/sandbox-core/package-lock.json +0 -136
  98. package/plugins/sandbox-core/package.json +0 -39
  99. package/plugins/sandbox-core/src/boxlite-context.ts +0 -2
  100. package/plugins/sandbox-core/src/boxlite-contribution.ts +0 -53
  101. package/plugins/sandbox-core/src/boxlite-executor.ts +0 -911
  102. package/plugins/sandbox-core/src/docker-contribution.ts +0 -43
  103. package/plugins/sandbox-core/src/docker-executor.ts +0 -217
  104. package/plugins/sandbox-core/tsconfig.json +0 -19
  105. package/scripts/local-extensions.mjs +0 -168
  106. package/src/agent/event-forwarder.ts +0 -414
  107. package/src/cli/commands/config.ts +0 -328
  108. package/src/cli/commands/configure.ts +0 -92
  109. package/src/cli/commands/cron.ts +0 -410
  110. package/src/cli/commands/doctor.ts +0 -230
  111. package/src/cli/commands/extension.ts +0 -205
  112. package/src/cli/commands/init.ts +0 -396
  113. package/src/cli/commands/start.ts +0 -223
  114. package/src/cli/commands/topology.ts +0 -383
  115. package/src/cli/index.ts +0 -9
  116. package/src/cli/program.ts +0 -465
  117. package/src/cli/shared/config-io.ts +0 -277
  118. package/src/cli/shared/config-mutators.ts +0 -440
  119. package/src/cli/shared/config-schema.ts +0 -228
  120. package/src/cli/shared/config-types.ts +0 -121
  121. package/src/cli/shared/configure-sections.ts +0 -551
  122. package/src/cli/shared/discord-config.ts +0 -14
  123. package/src/cli/shared/init-catalog.ts +0 -189
  124. package/src/cli/shared/init-models-file.ts +0 -77
  125. package/src/cli/shared/runtime.ts +0 -33
  126. package/src/cli/shared/schema-prompts.ts +0 -414
  127. package/src/cli/tests/config-command.test.ts +0 -56
  128. package/src/cli/tests/config-io.test.ts +0 -92
  129. package/src/cli/tests/config-mutators.test.ts +0 -59
  130. package/src/cli/tests/doctor.test.ts +0 -120
  131. package/src/cli/tests/init-catalog.test.ts +0 -96
  132. package/src/cli/tests/program-options.test.ts +0 -113
  133. package/src/cli/tests/routing-config.test.ts +0 -209
  134. package/src/core/control-command.ts +0 -12
  135. package/src/core/dedup-store.ts +0 -103
  136. package/src/core/gateway.ts +0 -607
  137. package/src/core/routing.ts +0 -379
  138. package/src/core/runtime-registry.ts +0 -141
  139. package/src/core/tests/control-command.test.ts +0 -20
  140. package/src/core/tests/runtime-registry.test.ts +0 -140
  141. package/src/core/tests/typing-controller.test.ts +0 -129
  142. package/src/core/types.ts +0 -318
  143. package/src/core/typing-controller.ts +0 -119
  144. package/src/cron/config.ts +0 -154
  145. package/src/cron/schedule.ts +0 -61
  146. package/src/cron/service.ts +0 -249
  147. package/src/cron/store.ts +0 -155
  148. package/src/cron/types.ts +0 -60
  149. package/src/extension/loader.ts +0 -145
  150. package/src/extension/manager.ts +0 -355
  151. package/src/extension/manifest.ts +0 -26
  152. package/src/extension/registry.ts +0 -229
  153. package/src/main.ts +0 -8
  154. package/src/sandbox/executor.ts +0 -44
  155. package/src/sandbox/host-executor.ts +0 -118
  156. package/tsconfig.json +0 -18
@@ -1,199 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { dirname, join } from "node:path";
5
- import test from "node:test";
6
- import { loadGatewayConfig } from "../../core/routing.js";
7
- async function writeTempConfig(payload) {
8
- const dir = await mkdtemp(join(tmpdir(), "dobby-routing-"));
9
- const configPath = join(dir, "gateway.json");
10
- await writeFile(configPath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
11
- return configPath;
12
- }
13
- async function writeRepoTempConfig(payload) {
14
- const repoRoot = await mkdtemp(join(tmpdir(), "dobby-routing-repo-"));
15
- const configDir = join(repoRoot, "config");
16
- await mkdir(configDir, { recursive: true });
17
- await mkdir(join(repoRoot, "scripts"), { recursive: true });
18
- await writeFile(join(repoRoot, "package.json"), JSON.stringify({ name: "dobby" }), "utf-8");
19
- await writeFile(join(repoRoot, "scripts", "local-extensions.mjs"), "#!/usr/bin/env node\n", "utf-8");
20
- const configPath = join(configDir, "gateway.json");
21
- await writeFile(configPath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
22
- return { repoRoot, configPath };
23
- }
24
- function validConfig() {
25
- return {
26
- extensions: { allowList: [] },
27
- providers: {
28
- default: "pi.main",
29
- items: {
30
- "pi.main": {
31
- type: "provider.pi",
32
- },
33
- },
34
- },
35
- connectors: {
36
- items: {
37
- "discord.main": {
38
- type: "connector.discord",
39
- botName: "dobby-main",
40
- botToken: "token",
41
- },
42
- },
43
- },
44
- sandboxes: {
45
- default: "host.builtin",
46
- items: {},
47
- },
48
- routes: {
49
- defaults: {
50
- provider: "pi.main",
51
- sandbox: "host.builtin",
52
- tools: "full",
53
- mentions: "required",
54
- },
55
- items: {
56
- main: {
57
- projectRoot: "./workspace/project-a",
58
- systemPromptFile: "./prompts/main.md",
59
- },
60
- },
61
- },
62
- bindings: {
63
- items: {
64
- "discord.main.main": {
65
- connector: "discord.main",
66
- source: {
67
- type: "channel",
68
- id: "123",
69
- },
70
- route: "main",
71
- },
72
- },
73
- },
74
- data: {
75
- rootDir: "./data",
76
- dedupTtlMs: 604800000,
77
- },
78
- };
79
- }
80
- test("loadGatewayConfig applies route defaults and resolves relative paths", async () => {
81
- const payload = validConfig();
82
- const configPath = await writeTempConfig(payload);
83
- try {
84
- const loaded = await loadGatewayConfig(configPath);
85
- const configDir = dirname(configPath);
86
- assert.equal(loaded.providers.default, "pi.main");
87
- assert.deepEqual(loaded.routes.defaults, {
88
- provider: "pi.main",
89
- sandbox: "host.builtin",
90
- tools: "full",
91
- mentions: "required",
92
- });
93
- assert.deepEqual(loaded.routes.items.main, {
94
- projectRoot: join(configDir, "workspace/project-a"),
95
- systemPromptFile: join(configDir, "prompts/main.md"),
96
- provider: "pi.main",
97
- sandbox: "host.builtin",
98
- tools: "full",
99
- mentions: "required",
100
- });
101
- assert.equal(loaded.data.rootDir, join(configDir, "data"));
102
- assert.deepEqual(loaded.bindings.items["discord.main.main"], {
103
- connector: "discord.main",
104
- source: {
105
- type: "channel",
106
- id: "123",
107
- },
108
- route: "main",
109
- });
110
- }
111
- finally {
112
- await rm(dirname(configPath), { recursive: true, force: true });
113
- }
114
- });
115
- test("loadGatewayConfig resolves data.rootDir from repo root for repo-local config/gateway.json", async () => {
116
- const payload = validConfig();
117
- const { repoRoot, configPath } = await writeRepoTempConfig(payload);
118
- try {
119
- const loaded = await loadGatewayConfig(configPath);
120
- const mainRoute = loaded.routes.items.main;
121
- assert.ok(mainRoute);
122
- assert.equal(loaded.data.rootDir, join(repoRoot, "data"));
123
- assert.equal(mainRoute.projectRoot, join(repoRoot, "workspace/project-a"));
124
- }
125
- finally {
126
- await rm(repoRoot, { recursive: true, force: true });
127
- }
128
- });
129
- test("loadGatewayConfig rejects connector fields reserved by the host", async () => {
130
- const payload = validConfig();
131
- payload.connectors = {
132
- items: {
133
- "discord.main": {
134
- type: "connector.discord",
135
- botName: "dobby-main",
136
- botToken: "token",
137
- botChannelMap: {
138
- "123": "main",
139
- },
140
- },
141
- },
142
- };
143
- const configPath = await writeTempConfig(payload);
144
- try {
145
- await assert.rejects(loadGatewayConfig(configPath), /must not include 'botChannelMap'/);
146
- }
147
- finally {
148
- await rm(dirname(configPath), { recursive: true, force: true });
149
- }
150
- });
151
- test("loadGatewayConfig rejects connector env indirection fields reserved by the host", async () => {
152
- const payload = validConfig();
153
- payload.connectors = {
154
- items: {
155
- "discord.main": {
156
- type: "connector.discord",
157
- botName: "dobby-main",
158
- botTokenEnv: "DISCORD_BOT_TOKEN",
159
- },
160
- },
161
- };
162
- const configPath = await writeTempConfig(payload);
163
- try {
164
- await assert.rejects(loadGatewayConfig(configPath), /must not include 'botTokenEnv'/);
165
- }
166
- finally {
167
- await rm(dirname(configPath), { recursive: true, force: true });
168
- }
169
- });
170
- test("loadGatewayConfig fails fast on duplicate binding sources", async () => {
171
- const payload = validConfig();
172
- payload.bindings = {
173
- items: {
174
- "discord.main.main": {
175
- connector: "discord.main",
176
- source: {
177
- type: "channel",
178
- id: "123",
179
- },
180
- route: "main",
181
- },
182
- "discord.main.duplicate": {
183
- connector: "discord.main",
184
- source: {
185
- type: "channel",
186
- id: "123",
187
- },
188
- route: "main",
189
- },
190
- },
191
- };
192
- const configPath = await writeTempConfig(payload);
193
- try {
194
- await assert.rejects(loadGatewayConfig(configPath), /duplicates source 'discord\.main:channel:123'/);
195
- }
196
- finally {
197
- await rm(dirname(configPath), { recursive: true, force: true });
198
- }
199
- });
@@ -1,191 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { dirname, join } from "node:path";
5
- import test from "node:test";
6
- import { loadGatewayConfig } from "../../core/routing.js";
7
- async function writeTempConfig(payload) {
8
- const dir = await mkdtemp(join(tmpdir(), "dobby-routing-"));
9
- const configPath = join(dir, "gateway.json");
10
- await writeFile(configPath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
11
- return configPath;
12
- }
13
- function validConfig() {
14
- return {
15
- extensions: { allowList: [] },
16
- providers: {
17
- default: "pi.main",
18
- items: {
19
- "pi.main": {
20
- type: "provider.pi",
21
- },
22
- },
23
- },
24
- connectors: {
25
- items: {
26
- "discord.main": {
27
- type: "connector.discord",
28
- botName: "dobby-main",
29
- botToken: "token",
30
- },
31
- },
32
- },
33
- sandboxes: {
34
- default: "host.builtin",
35
- items: {},
36
- },
37
- routes: {
38
- defaults: {
39
- provider: "pi.main",
40
- sandbox: "host.builtin",
41
- tools: "full",
42
- mentions: "required",
43
- },
44
- items: {
45
- main: {
46
- projectRoot: "./workspace/project-a",
47
- systemPromptFile: "./prompts/main.md",
48
- },
49
- },
50
- },
51
- bindings: {
52
- items: {
53
- "discord.main.main": {
54
- connector: "discord.main",
55
- source: {
56
- type: "channel",
57
- id: "123",
58
- },
59
- route: "main",
60
- },
61
- },
62
- },
63
- data: {
64
- rootDir: "./data",
65
- dedupTtlMs: 604800000,
66
- },
67
- };
68
- }
69
- test("loadGatewayConfig applies route defaults and resolves relative paths", async () => {
70
- const payload = validConfig();
71
- const configPath = await writeTempConfig(payload);
72
- try {
73
- const loaded = await loadGatewayConfig(configPath);
74
- const configDir = dirname(configPath);
75
- assert.equal(loaded.providers.default, "pi.main");
76
- assert.deepEqual(loaded.routes.defaults, {
77
- provider: "pi.main",
78
- sandbox: "host.builtin",
79
- tools: "full",
80
- mentions: "required",
81
- });
82
- assert.deepEqual(loaded.routes.items.main, {
83
- projectRoot: join(configDir, "workspace/project-a"),
84
- systemPromptFile: join(configDir, "prompts/main.md"),
85
- provider: "pi.main",
86
- sandbox: "host.builtin",
87
- tools: "full",
88
- mentions: "required",
89
- });
90
- assert.equal(loaded.data.rootDir, join(configDir, "data"));
91
- assert.deepEqual(loaded.bindings.items["discord.main.main"], {
92
- connector: "discord.main",
93
- source: {
94
- type: "channel",
95
- id: "123",
96
- },
97
- route: "main",
98
- });
99
- }
100
- finally {
101
- await rm(dirname(configPath), { recursive: true, force: true });
102
- }
103
- });
104
- test("loadGatewayConfig fails fast on legacy top-level routing", async () => {
105
- const payload = validConfig();
106
- payload.routing = {
107
- routes: {
108
- main: {
109
- projectRoot: process.cwd(),
110
- },
111
- },
112
- };
113
- const configPath = await writeTempConfig(payload);
114
- try {
115
- await assert.rejects(loadGatewayConfig(configPath), /top-level field 'routing'/);
116
- }
117
- finally {
118
- await rm(dirname(configPath), { recursive: true, force: true });
119
- }
120
- });
121
- test("loadGatewayConfig fails fast on legacy botTokenEnv", async () => {
122
- const payload = validConfig();
123
- payload.connectors = {
124
- items: {
125
- "discord.main": {
126
- type: "connector.discord",
127
- botName: "dobby-main",
128
- botTokenEnv: "DISCORD_BOT_TOKEN",
129
- },
130
- },
131
- };
132
- const configPath = await writeTempConfig(payload);
133
- try {
134
- await assert.rejects(loadGatewayConfig(configPath), /botTokenEnv/);
135
- }
136
- finally {
137
- await rm(dirname(configPath), { recursive: true, force: true });
138
- }
139
- });
140
- test("loadGatewayConfig fails fast on legacy connector route maps", async () => {
141
- const payload = validConfig();
142
- payload.connectors = {
143
- items: {
144
- "discord.main": {
145
- type: "connector.discord",
146
- botName: "dobby-main",
147
- botToken: "token",
148
- botChannelMap: {
149
- "123": "main",
150
- },
151
- },
152
- },
153
- };
154
- const configPath = await writeTempConfig(payload);
155
- try {
156
- await assert.rejects(loadGatewayConfig(configPath), /botChannelMap/);
157
- }
158
- finally {
159
- await rm(dirname(configPath), { recursive: true, force: true });
160
- }
161
- });
162
- test("loadGatewayConfig fails fast on duplicate binding sources", async () => {
163
- const payload = validConfig();
164
- payload.bindings = {
165
- items: {
166
- "discord.main.main": {
167
- connector: "discord.main",
168
- source: {
169
- type: "channel",
170
- id: "123",
171
- },
172
- route: "main",
173
- },
174
- "discord.main.duplicate": {
175
- connector: "discord.main",
176
- source: {
177
- type: "channel",
178
- id: "123",
179
- },
180
- route: "main",
181
- },
182
- },
183
- };
184
- const configPath = await writeTempConfig(payload);
185
- try {
186
- await assert.rejects(loadGatewayConfig(configPath), /duplicates source 'discord\.main:channel:123'/);
187
- }
188
- finally {
189
- await rm(dirname(configPath), { recursive: true, force: true });
190
- }
191
- });
@@ -1,17 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { parseControlCommand } from "../control-command.js";
4
- test("parseControlCommand recognizes cancel aliases", () => {
5
- assert.equal(parseControlCommand("stop"), "cancel");
6
- assert.equal(parseControlCommand(" /STOP "), "cancel");
7
- assert.equal(parseControlCommand("/cancel"), "cancel");
8
- });
9
- test("parseControlCommand recognizes new session aliases", () => {
10
- assert.equal(parseControlCommand("/new"), "new_session");
11
- assert.equal(parseControlCommand(" /reset "), "new_session");
12
- });
13
- test("parseControlCommand ignores regular messages", () => {
14
- assert.equal(parseControlCommand("please /new"), null);
15
- assert.equal(parseControlCommand(""), null);
16
- assert.equal(parseControlCommand("hello"), null);
17
- });
@@ -1,167 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { Gateway } from "../gateway.js";
4
- class FakeConnector {
5
- id = "connector.test";
6
- platform = "test";
7
- name = "test";
8
- capabilities;
9
- sent = [];
10
- sentCount = 0;
11
- constructor(updateStrategy) {
12
- this.capabilities = {
13
- updateStrategy,
14
- supportsThread: false,
15
- supportsTyping: false,
16
- supportsFileUpload: false,
17
- };
18
- }
19
- async start(_ctx) { }
20
- async send(message) {
21
- this.sent.push(message);
22
- this.sentCount += 1;
23
- return { messageId: `msg-${this.sentCount}` };
24
- }
25
- async stop() { }
26
- }
27
- class ThrowingRuntime {
28
- listener = null;
29
- subscribe(listener) {
30
- this.listener = listener;
31
- return () => {
32
- this.listener = null;
33
- };
34
- }
35
- async prompt() {
36
- this.listener?.({ type: "message_complete", text: "partial output" });
37
- throw new Error("boom");
38
- }
39
- async abort() { }
40
- dispose() {
41
- this.listener = null;
42
- }
43
- }
44
- const route = {
45
- routeId: "route.main",
46
- profile: {
47
- projectRoot: "/tmp/project",
48
- tools: "readonly",
49
- allowMentionsOnly: false,
50
- maxConcurrentTurns: 1,
51
- },
52
- };
53
- const noopLogger = {
54
- info: () => { },
55
- warn: () => { },
56
- error: () => { },
57
- debug: () => { },
58
- };
59
- const fakeExecutor = {
60
- exec: async () => ({ stdout: "", stderr: "", code: 0, killed: false }),
61
- spawn: () => {
62
- throw new Error("spawn should not be called in this test");
63
- },
64
- close: async () => { },
65
- };
66
- const fakeDedupStore = {
67
- has: () => false,
68
- add: () => { },
69
- load: async () => { },
70
- startAutoFlush: () => { },
71
- stopAutoFlush: () => { },
72
- flush: async () => { },
73
- };
74
- const fakeRuntimeRegistry = {
75
- getOrCreate: async () => {
76
- throw new Error("getOrCreate should not be called for scheduled stateless runs");
77
- },
78
- enqueue: async () => {
79
- throw new Error("enqueue should not be called for scheduled stateless runs");
80
- },
81
- abort: async () => false,
82
- closeAll: async () => { },
83
- };
84
- function buildGateway(updateStrategy) {
85
- const connector = new FakeConnector(updateStrategy);
86
- const provider = {
87
- id: "provider.main",
88
- createRuntime: async () => new ThrowingRuntime(),
89
- };
90
- const config = {
91
- extensions: { allowList: [] },
92
- providers: {
93
- defaultProviderId: "provider.main",
94
- instances: {},
95
- },
96
- connectors: {
97
- instances: {},
98
- },
99
- sandboxes: {
100
- defaultSandboxId: "sandbox.main",
101
- instances: {},
102
- },
103
- routing: {
104
- routes: {
105
- [route.routeId]: route.profile,
106
- },
107
- },
108
- data: {
109
- rootDir: "/tmp",
110
- sessionsDir: "/tmp/sessions",
111
- attachmentsDir: "/tmp/attachments",
112
- logsDir: "/tmp/logs",
113
- stateDir: "/tmp/state",
114
- dedupTtlMs: 60_000,
115
- },
116
- };
117
- const routeResolver = {
118
- resolve: (routeId) => (routeId === route.routeId ? route : null),
119
- };
120
- const gateway = new Gateway({
121
- config,
122
- connectors: [connector],
123
- providers: new Map([["provider.main", provider]]),
124
- executors: new Map([["sandbox.main", fakeExecutor]]),
125
- routeResolver,
126
- dedupStore: fakeDedupStore,
127
- runtimeRegistry: fakeRuntimeRegistry,
128
- logger: noopLogger,
129
- });
130
- return { gateway, connector };
131
- }
132
- async function runScheduledAndCollect(updateStrategy) {
133
- const { gateway, connector } = buildGateway(updateStrategy);
134
- await gateway.handleScheduled({
135
- jobId: "job-1",
136
- runId: `run-${updateStrategy}`,
137
- connectorId: connector.id,
138
- routeId: route.routeId,
139
- channelId: "chat-1",
140
- prompt: "hello",
141
- });
142
- return connector;
143
- }
144
- test("gateway error path uses update for edit strategy", async () => {
145
- const connector = await runScheduledAndCollect("edit");
146
- assert.equal(connector.sent.length, 2);
147
- assert.equal(connector.sent[0]?.mode, "create");
148
- assert.equal(connector.sent[0]?.text, "partial output");
149
- assert.equal(connector.sent[1]?.mode, "update");
150
- assert.equal(connector.sent[1]?.targetMessageId, "msg-1");
151
- assert.equal(connector.sent[1]?.text, "Error: boom");
152
- });
153
- test("gateway error path uses create for append strategy", async () => {
154
- const connector = await runScheduledAndCollect("append");
155
- assert.equal(connector.sent.length, 2);
156
- assert.equal(connector.sent[0]?.mode, "create");
157
- assert.equal(connector.sent[0]?.text, "partial output");
158
- assert.equal(connector.sent[1]?.mode, "create");
159
- assert.equal(connector.sent[1]?.replyToMessageId, "msg-1");
160
- assert.equal(connector.sent[1]?.text, "Error: boom");
161
- });
162
- test("gateway error path uses create for final_only strategy", async () => {
163
- const connector = await runScheduledAndCollect("final_only");
164
- assert.equal(connector.sent.length, 1);
165
- assert.equal(connector.sent[0]?.mode, "create");
166
- assert.equal(connector.sent[0]?.text, "Error: boom");
167
- });
@@ -1,116 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { RuntimeRegistry } from "../runtime-registry.js";
4
- function deferred() {
5
- let resolve;
6
- const promise = new Promise((res) => {
7
- resolve = res;
8
- });
9
- return { promise, resolve };
10
- }
11
- function createLogger() {
12
- return {
13
- error() { },
14
- warn() { },
15
- info() { },
16
- debug() { },
17
- };
18
- }
19
- function createConversationRuntime(id, abortCalls, closeCalls) {
20
- return {
21
- key: id,
22
- routeId: "route.main",
23
- route: {
24
- projectRoot: "/tmp/project",
25
- tools: "full",
26
- mentions: "required",
27
- provider: "provider.main",
28
- sandbox: "host.builtin",
29
- },
30
- providerId: "provider.main",
31
- sandboxId: "host.builtin",
32
- runtime: {
33
- async prompt() { },
34
- subscribe() {
35
- return () => { };
36
- },
37
- async abort() {
38
- abortCalls.push(id);
39
- },
40
- dispose() { },
41
- },
42
- async close() {
43
- closeCalls.push(id);
44
- },
45
- };
46
- }
47
- test("cancel aborts the active run and drops queued turns", async () => {
48
- const registry = new RuntimeRegistry(createLogger());
49
- const abortCalls = [];
50
- const closeCalls = [];
51
- let runtimeCount = 0;
52
- const createRuntime = async () => {
53
- runtimeCount += 1;
54
- return createConversationRuntime(`runtime-${runtimeCount}`, abortCalls, closeCalls);
55
- };
56
- const firstStarted = deferred();
57
- const releaseFirst = deferred();
58
- const started = [];
59
- const firstTurn = registry.run("conversation", createRuntime, async (runtime) => {
60
- started.push(runtime.key);
61
- firstStarted.resolve(undefined);
62
- await releaseFirst.promise;
63
- });
64
- await firstStarted.promise;
65
- const secondTurn = registry.run("conversation", createRuntime, async (runtime) => {
66
- started.push(runtime.key);
67
- });
68
- const thirdTurn = registry.run("conversation", createRuntime, async (runtime) => {
69
- started.push(runtime.key);
70
- });
71
- assert.equal(await registry.cancel("conversation"), true);
72
- assert.deepEqual(abortCalls, ["runtime-1"]);
73
- releaseFirst.resolve(undefined);
74
- await Promise.all([firstTurn, secondTurn, thirdTurn]);
75
- assert.deepEqual(started, ["runtime-1"]);
76
- assert.deepEqual(closeCalls, []);
77
- await registry.run("conversation", createRuntime, async (runtime) => {
78
- started.push(runtime.key);
79
- });
80
- assert.deepEqual(started, ["runtime-1", "runtime-1"]);
81
- assert.equal(runtimeCount, 1);
82
- });
83
- test("reset closes the current runtime and recreates it on the next turn", async () => {
84
- const registry = new RuntimeRegistry(createLogger());
85
- const abortCalls = [];
86
- const closeCalls = [];
87
- let runtimeCount = 0;
88
- const createRuntime = async () => {
89
- runtimeCount += 1;
90
- return createConversationRuntime(`runtime-${runtimeCount}`, abortCalls, closeCalls);
91
- };
92
- const firstStarted = deferred();
93
- const releaseFirst = deferred();
94
- const started = [];
95
- const firstTurn = registry.run("conversation", createRuntime, async (runtime) => {
96
- started.push(runtime.key);
97
- firstStarted.resolve(undefined);
98
- await releaseFirst.promise;
99
- });
100
- await firstStarted.promise;
101
- const queuedTurn = registry.run("conversation", createRuntime, async (runtime) => {
102
- started.push(runtime.key);
103
- });
104
- const resetPromise = registry.reset("conversation");
105
- releaseFirst.resolve(undefined);
106
- assert.equal(await resetPromise, true);
107
- await Promise.all([firstTurn, queuedTurn]);
108
- assert.deepEqual(abortCalls, ["runtime-1"]);
109
- assert.deepEqual(closeCalls, ["runtime-1"]);
110
- assert.deepEqual(started, ["runtime-1"]);
111
- await registry.run("conversation", createRuntime, async (runtime) => {
112
- started.push(runtime.key);
113
- });
114
- assert.deepEqual(started, ["runtime-1", "runtime-2"]);
115
- assert.equal(runtimeCount, 2);
116
- });