@app-vitals/agent-runtime 0.0.1 → 0.1.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 (160) hide show
  1. package/Dockerfile +265 -0
  2. package/agent-account-status-ref.ts +53 -0
  3. package/agent-account-status-ref.unit.test.ts +24 -0
  4. package/agent-extension.ts +144 -0
  5. package/agent-extension.unit.test.ts +139 -0
  6. package/agent-repos-ref.ts +63 -0
  7. package/agent-repos-ref.unit.test.ts +84 -0
  8. package/agent-slack-membership-ref.ts +94 -0
  9. package/agent-slack-membership-ref.unit.test.ts +201 -0
  10. package/agent-trial-expiry-ref.ts +63 -0
  11. package/agent-trial-expiry-ref.unit.test.ts +84 -0
  12. package/anthropic-federation-guard.ts +72 -0
  13. package/anthropic-federation-guard.unit.test.ts +127 -0
  14. package/base-image-contract.smoke.test.ts +149 -0
  15. package/boot.ts +845 -0
  16. package/browser.integration.test.ts +52 -0
  17. package/browser.ts +34 -0
  18. package/chat-poller.integration.test.ts +109 -0
  19. package/chat-poller.ts +401 -0
  20. package/chat-poller.unit.test.ts +1581 -0
  21. package/chat-token-reporter.integration.test.ts +282 -0
  22. package/chat-token-reporter.ts +136 -0
  23. package/claude-version.ts +55 -0
  24. package/claude-version.unit.test.ts +73 -0
  25. package/claude.ts +1054 -0
  26. package/claude.unit.test.ts +3042 -0
  27. package/cleanup-after-days-ref.ts +83 -0
  28. package/cleanup-after-days-ref.unit.test.ts +89 -0
  29. package/cleanup-merged-worktrees-ref.ts +75 -0
  30. package/cleanup-merged-worktrees-ref.unit.test.ts +72 -0
  31. package/cli-args.ts +36 -0
  32. package/cli-args.unit.test.ts +94 -0
  33. package/clock.ts +47 -0
  34. package/config.ts +99 -0
  35. package/config.unit.test.ts +356 -0
  36. package/context-stamp.ts +189 -0
  37. package/context-stamp.unit.test.ts +159 -0
  38. package/cron-failure-reporter.ts +107 -0
  39. package/cron-failure-reporter.unit.test.ts +244 -0
  40. package/cron-handler.integration.test.ts +614 -0
  41. package/cron-handler.smoke.test.ts +1371 -0
  42. package/cron-handler.ts +750 -0
  43. package/cron-handler.unit.test.ts +1177 -0
  44. package/cron-run-reporter.integration.test.ts +806 -0
  45. package/cron-run-reporter.ts +398 -0
  46. package/crypto.ts +73 -0
  47. package/cutover-validate.integration.test.ts +245 -0
  48. package/cutover-validate.ts +84 -0
  49. package/entrypoint-main.ts +79 -0
  50. package/entrypoint.integration.test.ts +315 -0
  51. package/entrypoint.ts +154 -0
  52. package/errors.ts +61 -0
  53. package/fixtures/chat-service/attachment.json +15 -0
  54. package/fixtures/chat-service/messages.json +86 -0
  55. package/fixtures/chat-service/threads.json +50 -0
  56. package/fixtures/github-installations/installations.json +1231 -0
  57. package/fixtures/slack-provision-cassette.json +6 -0
  58. package/fixtures/specialties/fixture/manifest.yaml +35 -0
  59. package/fixtures/specialties/fixture/templates/CLAUDE.md.template +7 -0
  60. package/fixtures/specialties/fixture/templates/IDENTITY.md.template +8 -0
  61. package/fixtures/specialties/fixture/templates/SOUL.md.template +45 -0
  62. package/fixtures/stream-json/clean-exit-no-result.ts +53 -0
  63. package/fixtures/stream-json/clean-multi-turn.ts +209 -0
  64. package/fixtures/stream-json/progress-phases.ts +203 -0
  65. package/fixtures/stream-json/skill-attribution.ts +251 -0
  66. package/fixtures/stream-json/truncated-no-result.ts +84 -0
  67. package/format.ts +283 -0
  68. package/format.unit.test.ts +558 -0
  69. package/gh-helpers.ts +139 -0
  70. package/gh-helpers.unit.test.ts +253 -0
  71. package/gh-token-files.integration.test.ts +143 -0
  72. package/gh-token-files.ts +92 -0
  73. package/gh-token-files.unit.test.ts +63 -0
  74. package/github-app-auth.integration.test.ts +419 -0
  75. package/github-app-auth.ts +193 -0
  76. package/github-auth-deps.ts +128 -0
  77. package/github-auth-startup.ts +231 -0
  78. package/github-auth-startup.unit.test.ts +325 -0
  79. package/github-installations-manager.integration.test.ts +323 -0
  80. package/github-installations-reporter.integration.test.ts +124 -0
  81. package/github-installations-reporter.ts +150 -0
  82. package/github-installations-reporter.unit.test.ts +166 -0
  83. package/github-installations.integration.test.ts +88 -0
  84. package/github-installations.ts +462 -0
  85. package/github-installations.unit.test.ts +98 -0
  86. package/health.smoke.test.ts +244 -0
  87. package/health.ts +193 -0
  88. package/health.unit.test.ts +40 -0
  89. package/http-chat-service-client.integration.test.ts +345 -0
  90. package/http-chat-service-client.ts +287 -0
  91. package/http-chat-service-client.unit.test.ts +500 -0
  92. package/log-prefix.ts +12 -0
  93. package/log-prefix.unit.test.ts +24 -0
  94. package/markers.ts +178 -0
  95. package/markers.unit.test.ts +374 -0
  96. package/package.json +50 -4
  97. package/piper-voice.ts +116 -0
  98. package/piper-voice.unit.test.ts +124 -0
  99. package/pricing.ts +7 -0
  100. package/pricing.unit.test.ts +149 -0
  101. package/process-tree-kill.integration.test.ts +133 -0
  102. package/process-tree-kill.ts +325 -0
  103. package/process-tree-kill.unit.test.ts +265 -0
  104. package/progress-milestones.ts +86 -0
  105. package/progress-milestones.unit.test.ts +188 -0
  106. package/run-telemetry.ts +241 -0
  107. package/run-telemetry.unit.test.ts +235 -0
  108. package/sessions.integration.test.ts +273 -0
  109. package/sessions.ts +191 -0
  110. package/sessions.unit.test.ts +94 -0
  111. package/setup-github-auth.integration.test.ts +858 -0
  112. package/setup-github-auth.ts +297 -0
  113. package/setup.fixture.integration.test.ts +109 -0
  114. package/setup.integration.test.ts +1495 -0
  115. package/setup.ts +853 -0
  116. package/setup.unit.test.ts +286 -0
  117. package/shipwright-config-client.ts +36 -0
  118. package/shipwright-runtime-client.integration.test.ts +336 -0
  119. package/shipwright-runtime-client.ts +118 -0
  120. package/slack-account-status.unit.test.ts +251 -0
  121. package/slack-bolt-client.integration.test.ts +76 -0
  122. package/slack-chat-conversation.integration.test.ts +333 -0
  123. package/slack-progress.ts +510 -0
  124. package/slack-progress.unit.test.ts +806 -0
  125. package/slack-socket-mode-ping.integration.test.ts +137 -0
  126. package/slack-startup.ts +170 -0
  127. package/slack-startup.unit.test.ts +300 -0
  128. package/slack-thread-status-tracker.ts +70 -0
  129. package/slack-thread-status-tracker.unit.test.ts +118 -0
  130. package/slack-trial-expiry.unit.test.ts +269 -0
  131. package/slack.integration.test.ts +3979 -0
  132. package/slack.ts +1286 -0
  133. package/slack.unit.test.ts +1070 -0
  134. package/spawn-env.ts +58 -0
  135. package/spawn-env.unit.test.ts +96 -0
  136. package/startup-dm.integration.test.ts +156 -0
  137. package/startup-dm.ts +29 -0
  138. package/sync-config-clone.ts +238 -0
  139. package/sync-config-clone.unit.test.ts +429 -0
  140. package/test-env.ts +27 -0
  141. package/users.ts +90 -0
  142. package/users.unit.test.ts +108 -0
  143. package/voice.integration.test.ts +669 -0
  144. package/voice.ts +315 -0
  145. package/voice.unit.test.ts +105 -0
  146. package/workspace/BOOTSTRAP.md.template +43 -0
  147. package/workspace/BOOTSTRAP.md.template.content.test.ts +48 -0
  148. package/workspace/IDENTITY.md.template +8 -0
  149. package/workspace/RUNTIME.md.template +193 -0
  150. package/workspace/RUNTIME.md.template.content.test.ts +75 -0
  151. package/workspace/SOUL.md.template +45 -0
  152. package/workspace/SPECIALTY.md.template +37 -0
  153. package/workspace/SPECIALTY.md.template.content.test.ts +50 -0
  154. package/workspace/VOICE.md.template +8 -0
  155. package/workspace/mise.toml.template +12 -0
  156. package/workspace/requirements.txt.template +1 -0
  157. package/workspace-paths.ts +37 -0
  158. package/worktree-reaper.ts +244 -0
  159. package/worktree-reaper.unit.test.ts +380 -0
  160. package/README.md +0 -3
@@ -0,0 +1,333 @@
1
+ /**
2
+ * Integration coverage for the Slack chat conversation journey (T-027).
3
+ *
4
+ * Scope note: the task description's "chat-service thread -> agent claims"
5
+ * phrasing does not correspond to any real code path in this repo —
6
+ * createSlackApp() (agent/src/runtime/slack.ts) never touches ChatServiceClient, and
7
+ * chat-poller.ts (the chat-service flow) never touches Slack/Bolt/WebClient.
8
+ * This suite instead exercises the REAL, buildable journey: a multi-turn
9
+ * Slack conversation flowing end-to-end through createSlackApp — incoming
10
+ * Bolt message/app_mention event -> injected Claude runner -> reply posted
11
+ * via the mocked WebClient (say / client.chat.postMessage) -- verifying
12
+ * session continuity across turns via sessions.ts's threadKey +
13
+ * getSessionFn wiring.
14
+ *
15
+ * Strategy mirrors slack.integration.test.ts: inject all dependencies via
16
+ * createSlackApp's params (MockApp Bolt factory + mocked WebClient-shaped
17
+ * client). No mock.module(), no real Bolt socket, no real HTTP.
18
+ */
19
+
20
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
21
+ import { createAgentSlackMembershipRef } from "./agent-slack-membership-ref.ts";
22
+ import type { ModelUsage, TokenUsage } from "./claude.ts";
23
+ import { threadKey } from "./sessions.ts";
24
+ import { createSlackApp as _createSlackApp } from "./slack.ts";
25
+
26
+ // ─── Mock runner ────────────────────────────────────────────────────────────
27
+
28
+ const mockRunClaude = mock(
29
+ async (
30
+ _msg: string,
31
+ _key?: string,
32
+ ): Promise<{
33
+ result: string;
34
+ sessionId?: string;
35
+ usage?: TokenUsage;
36
+ totalCostUsd?: number;
37
+ modelUsage?: ModelUsage;
38
+ }> => ({ result: "Claude response text" }),
39
+ );
40
+
41
+ const identityFormatter = (text: string): string => text;
42
+
43
+ // ─── Test slack config ──────────────────────────────────────────────────────
44
+
45
+ const mockSlackConfig = {
46
+ botToken: "xoxb-test-token",
47
+ appToken: "xapp-test-token",
48
+ signingSecret: "test-secret",
49
+ };
50
+
51
+ // ─── MockApp — captures registered handlers (same pattern as slack.integration.test.ts) ───
52
+
53
+ type HandlerFn = (...args: unknown[]) => Promise<void>;
54
+
55
+ let capturedMessageHandler: HandlerFn | null = null;
56
+ let capturedMentionHandler: HandlerFn | null = null;
57
+
58
+ class MockApp {
59
+ constructor(_args: Record<string, unknown>) {
60
+ capturedMessageHandler = null;
61
+ capturedMentionHandler = null;
62
+ }
63
+
64
+ message(handler: HandlerFn) {
65
+ capturedMessageHandler = handler;
66
+ }
67
+
68
+ event(type: string, handler: HandlerFn) {
69
+ if (type === "app_mention") capturedMentionHandler = handler;
70
+ }
71
+ }
72
+
73
+ // Wrap with test deps — mirrors the createSlackApp() wrapper in
74
+ // slack.integration.test.ts, but only exposes the params this suite needs.
75
+ function createSlackApp(
76
+ getSessionFn: (key: string) => Promise<string | undefined>,
77
+ ) {
78
+ return _createSlackApp(
79
+ mockRunClaude,
80
+ identityFormatter,
81
+ threadKey,
82
+ // biome-ignore lint/suspicious/noExplicitAny: mock factory for tests
83
+ (cfg) => new MockApp(cfg as Record<string, unknown>) as any,
84
+ mockSlackConfig,
85
+ undefined, // sentryClient
86
+ async () => null, // fileDownloaderFn
87
+ {}, // voiceConfig
88
+ async () => null, // transcribeAudioFn
89
+ async () => null, // synthesizeSpeechFn
90
+ async (userId: string) => userId, // resolveUserFn
91
+ "UBOT123", // botUserId
92
+ async () => ({ messages: [] }), // conversationsRepliesFn
93
+ getSessionFn,
94
+ undefined, // blocksConverter — default
95
+ undefined, // chatTokenReporter — default noop
96
+ async () => undefined, // resolveUserEmailFn
97
+ // A fresh, unsynced ref — never the process-wide agentSlackMembershipRef
98
+ // singleton, which agent-slack-membership-ref.unit.test.ts mutates
99
+ // directly (see slack.integration.test.ts's identical fix).
100
+ createAgentSlackMembershipRef(),
101
+ );
102
+ }
103
+
104
+ // ─── Mock client helpers (same shape as slack.integration.test.ts) ─────────
105
+
106
+ function makeMockClient() {
107
+ return {
108
+ agents: {
109
+ sessions: {
110
+ setStatus: mock(async (_args: unknown) => {}),
111
+ },
112
+ },
113
+ chat: {
114
+ postMessage: mock(async (_args: unknown) => ({ ts: "posted.ts.1" })),
115
+ },
116
+ };
117
+ }
118
+
119
+ function makeSay(replyTs = "reply.ts.1") {
120
+ return mock(async (_args: unknown) => ({ ts: replyTs }));
121
+ }
122
+
123
+ describe("Slack chat conversation journey — DM multi-turn continuity", () => {
124
+ beforeEach(() => {
125
+ mockRunClaude.mockClear();
126
+ });
127
+
128
+ test("turn 1 (no prior session) then turn 2 (established session) both reach Claude and both post replies, threaded on the same sessionKey", async () => {
129
+ // Turn 1: brand-new DM — no session established yet.
130
+ const noSession = mock(async (_key: string) => undefined);
131
+ createSlackApp(noSession);
132
+
133
+ const client1 = makeMockClient();
134
+ const say1 = makeSay("reply.ts.turn1");
135
+ mockRunClaude.mockResolvedValueOnce({
136
+ result: "Hi! How can I help?",
137
+ sessionId: "sess-established-1",
138
+ });
139
+
140
+ await capturedMessageHandler?.({
141
+ message: {
142
+ channel: "D100",
143
+ ts: "1000.001",
144
+ text: "Hey, can you look into the failing build?",
145
+ channel_type: "im",
146
+ user: "U1",
147
+ },
148
+ say: say1,
149
+ client: client1,
150
+ });
151
+
152
+ // Turn 1 assertions: runner invoked with the DM's own sessionKey (thread
153
+ // root = channel:ts, since no thread_ts on the first message), reply posted.
154
+ expect(mockRunClaude).toHaveBeenCalledTimes(1);
155
+ expect(mockRunClaude).toHaveBeenCalledWith(
156
+ "[U1]: Hey, can you look into the failing build?",
157
+ "D100:1000.001",
158
+ expect.any(Function),
159
+ { SLACK_CHANNEL_ID: "D100", SLACK_THREAD_TS: "1000.001" },
160
+ );
161
+ expect(say1).toHaveBeenCalledWith({
162
+ text: "Hi! How can I help?",
163
+ thread_ts: "1000.001",
164
+ });
165
+
166
+ // Turn 2: a follow-up DM in the SAME channel/thread. Simulate that the
167
+ // session from turn 1 is now "established" — getSessionFn now returns the
168
+ // fixed session id for this thread's key, proving continuity is driven by
169
+ // the injected session lookup, not implicit in-process state.
170
+ const establishedKey = threadKey("D100", "1000.001");
171
+ const establishedSession = mock(async (key: string) =>
172
+ key === establishedKey ? "sess-established-1" : undefined,
173
+ );
174
+ createSlackApp(establishedSession);
175
+
176
+ const client2 = makeMockClient();
177
+ const say2 = makeSay("reply.ts.turn2");
178
+ mockRunClaude.mockResolvedValueOnce({
179
+ result: "Found it — the linter step is timing out.",
180
+ sessionId: "sess-established-1",
181
+ });
182
+
183
+ await capturedMessageHandler?.({
184
+ message: {
185
+ channel: "D100",
186
+ ts: "1000.002",
187
+ thread_ts: "1000.001", // Slack threads DM follow-ups under the root ts
188
+ text: "Any update?",
189
+ channel_type: "im",
190
+ user: "U1",
191
+ },
192
+ say: say2,
193
+ client: client2,
194
+ });
195
+
196
+ // Turn 2 assertions: runner invoked with the SAME sessionKey as turn 1
197
+ // (continuity), and the reply is posted back threaded on the same root ts.
198
+ expect(mockRunClaude).toHaveBeenCalledTimes(2);
199
+ expect(mockRunClaude).toHaveBeenLastCalledWith(
200
+ "[U1]: Any update?",
201
+ establishedKey,
202
+ expect.any(Function),
203
+ { SLACK_CHANNEL_ID: "D100", SLACK_THREAD_TS: "1000.001" },
204
+ );
205
+ expect(say2).toHaveBeenCalledWith({
206
+ text: "Found it — the linter step is timing out.",
207
+ thread_ts: "1000.001",
208
+ });
209
+
210
+ // Sanity: if session continuity were broken (e.g. sessionKey recomputed
211
+ // per-turn without reference to the established thread root), turn 2's
212
+ // sessionKey would differ from turn 1's. Confirm they match exactly.
213
+ const turn1SessionKey = mockRunClaude.mock.calls[0][1];
214
+ const turn2SessionKey = mockRunClaude.mock.calls[1][1];
215
+ expect(turn2SessionKey).toBe(turn1SessionKey);
216
+ });
217
+ });
218
+
219
+ describe("Slack chat conversation journey — channel app_mention thread continuity", () => {
220
+ beforeEach(() => {
221
+ mockRunClaude.mockClear();
222
+ });
223
+
224
+ test("initial @mention (no session) then a plain follow-up message in the same thread (established session) both reach Claude on the mention's threadKey", async () => {
225
+ // Turn 1: an @mention in a channel starts a new thread — no session yet.
226
+ const noSession = mock(async (_key: string) => undefined);
227
+ createSlackApp(noSession);
228
+
229
+ const client1 = makeMockClient();
230
+ const say1 = makeSay("mention.reply.ts");
231
+ mockRunClaude.mockResolvedValueOnce({
232
+ result: "On it — checking the deploy logs.",
233
+ sessionId: "sess-thread-77",
234
+ });
235
+
236
+ await capturedMentionHandler?.({
237
+ event: {
238
+ text: "<@UBOT123> can you check the deploy?",
239
+ channel: "C500",
240
+ ts: "2000.001",
241
+ user: "U2",
242
+ },
243
+ say: say1,
244
+ client: client1,
245
+ });
246
+
247
+ const mentionKey = threadKey("C500", "2000.001");
248
+ expect(mockRunClaude).toHaveBeenCalledTimes(1);
249
+ expect(mockRunClaude).toHaveBeenCalledWith(
250
+ "[U2]: <@UBOT123> can you check the deploy?",
251
+ mentionKey,
252
+ expect.any(Function),
253
+ { SLACK_CHANNEL_ID: "C500", SLACK_THREAD_TS: "2000.001" },
254
+ );
255
+ expect(say1).toHaveBeenCalledWith({
256
+ text: "On it — checking the deploy logs.",
257
+ thread_ts: "2000.001",
258
+ });
259
+
260
+ // Turn 2: a plain (non-@mention) follow-up posted inside that same thread.
261
+ // The channel message handler only routes thread replies when a session
262
+ // is already established for that thread — simulate turn 1 having
263
+ // established it, via getSessionFn now resolving the mention's threadKey.
264
+ const establishedSession = mock(async (key: string) =>
265
+ key === mentionKey ? "sess-thread-77" : undefined,
266
+ );
267
+ createSlackApp(establishedSession);
268
+
269
+ const client2 = makeMockClient();
270
+ const say2 = makeSay("followup.reply.ts");
271
+ mockRunClaude.mockResolvedValueOnce({
272
+ result: "Deploy looks healthy now.",
273
+ sessionId: "sess-thread-77",
274
+ });
275
+
276
+ await capturedMessageHandler?.({
277
+ message: {
278
+ channel: "C500",
279
+ ts: "2000.002",
280
+ thread_ts: "2000.001",
281
+ text: "thanks, any update?",
282
+ channel_type: "channel",
283
+ user: "U2",
284
+ },
285
+ say: say2,
286
+ client: client2,
287
+ });
288
+
289
+ expect(mockRunClaude).toHaveBeenCalledTimes(2);
290
+ expect(mockRunClaude).toHaveBeenLastCalledWith(
291
+ "[Thread message — respond normally, or use [silent] if no response is needed]\n[U2]: thanks, any update?",
292
+ mentionKey,
293
+ expect.any(Function),
294
+ { SLACK_CHANNEL_ID: "C500", SLACK_THREAD_TS: "2000.001" },
295
+ );
296
+ expect(say2).toHaveBeenCalledWith({
297
+ text: "Deploy looks healthy now.",
298
+ thread_ts: "2000.001",
299
+ });
300
+
301
+ // Continuity check: both turns keyed identically off the mention's thread root.
302
+ const turn1SessionKey = mockRunClaude.mock.calls[0][1];
303
+ const turn2SessionKey = mockRunClaude.mock.calls[1][1];
304
+ expect(turn2SessionKey).toBe(turn1SessionKey);
305
+ expect(turn2SessionKey).toBe(mentionKey);
306
+ });
307
+
308
+ test("a follow-up @mention in an already-established thread is dropped — the channel message handler owns it, avoiding a double response", async () => {
309
+ // Once a thread has a session, a redundant @mention on a later message in
310
+ // that same thread must NOT also trigger a second Claude call/reply —
311
+ // the message handler already covers thread continuity.
312
+ const establishedSession = mock(async (_key: string) => "sess-thread-77");
313
+ createSlackApp(establishedSession);
314
+
315
+ const client = makeMockClient();
316
+ const say = makeSay();
317
+
318
+ await capturedMentionHandler?.({
319
+ event: {
320
+ text: "<@UBOT123> one more thing",
321
+ channel: "C500",
322
+ ts: "2000.003",
323
+ thread_ts: "2000.001",
324
+ user: "U2",
325
+ },
326
+ say,
327
+ client,
328
+ });
329
+
330
+ expect(mockRunClaude).not.toHaveBeenCalled();
331
+ expect(say).not.toHaveBeenCalled();
332
+ });
333
+ });