@vellumai/assistant 0.4.29 → 0.4.30

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 (174) hide show
  1. package/ARCHITECTURE.md +39 -37
  2. package/README.md +5 -6
  3. package/docs/runbook-trusted-contacts.md +79 -43
  4. package/package.json +1 -1
  5. package/scripts/ipc/check-swift-decoder-drift.ts +2 -3
  6. package/scripts/test.sh +1 -1
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +4 -37
  8. package/src/__tests__/actor-token-service.test.ts +4 -3
  9. package/src/__tests__/app-executors.test.ts +7 -17
  10. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -10
  11. package/src/__tests__/browser-skill-endstate.test.ts +10 -1
  12. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -0
  13. package/src/__tests__/channel-approval-routes.test.ts +44 -44
  14. package/src/__tests__/channel-approval.test.ts +8 -0
  15. package/src/__tests__/channel-approvals.test.ts +39 -1
  16. package/src/__tests__/channel-guardian.test.ts +15 -5
  17. package/src/__tests__/channel-reply-delivery.test.ts +31 -0
  18. package/src/__tests__/commit-message-enrichment-service.test.ts +4 -0
  19. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +9 -0
  20. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  21. package/src/__tests__/gemini-image-service.test.ts +2 -2
  22. package/src/__tests__/guardian-grant-minting.test.ts +6 -6
  23. package/src/__tests__/guardian-routing-invariants.test.ts +34 -11
  24. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +4 -6
  25. package/src/__tests__/inbound-invite-redemption.test.ts +1 -1
  26. package/src/__tests__/integrations-cli.test.ts +3 -27
  27. package/src/__tests__/intent-routing.test.ts +3 -0
  28. package/src/__tests__/invite-redemption-service.test.ts +1 -1
  29. package/src/__tests__/{ingress-routes-http.test.ts → invite-routes-http.test.ts} +40 -320
  30. package/src/__tests__/ipc-snapshot.test.ts +4 -31
  31. package/src/__tests__/nl-approval-parser.test.ts +305 -0
  32. package/src/__tests__/oauth-provider-profiles.test.ts +34 -0
  33. package/src/__tests__/provider-error-scenarios.test.ts +68 -0
  34. package/src/__tests__/relay-server.test.ts +1 -1
  35. package/src/__tests__/retry-after-extraction.test.ts +111 -0
  36. package/src/__tests__/script-proxy-profile-template-fallback.test.ts +127 -0
  37. package/src/__tests__/session-media-retry.test.ts +147 -0
  38. package/src/__tests__/skill-feature-flags-integration.test.ts +9 -5
  39. package/src/__tests__/skill-feature-flags.test.ts +18 -12
  40. package/src/__tests__/skill-load-feature-flag.test.ts +4 -3
  41. package/src/__tests__/slack-block-formatting.test.ts +100 -0
  42. package/src/__tests__/slack-inbound-verification.test.ts +346 -0
  43. package/src/__tests__/slack-reaction-approvals.test.ts +77 -0
  44. package/src/__tests__/slack-skill.test.ts +3 -2
  45. package/src/__tests__/starter-task-flow.test.ts +0 -1
  46. package/src/__tests__/trusted-contact-verification.test.ts +3 -1
  47. package/src/__tests__/voice-invite-redemption.test.ts +1 -1
  48. package/src/amazon/client.ts +7 -24
  49. package/src/calls/relay-server.ts +39 -11
  50. package/src/channels/config.ts +1 -1
  51. package/src/cli/integrations.ts +10 -66
  52. package/src/config/bundled-skills/app-builder/SKILL.md +193 -1500
  53. package/src/config/bundled-skills/app-builder/TOOLS.json +70 -18
  54. package/src/config/bundled-skills/browser/TOOLS.json +59 -2
  55. package/src/config/bundled-skills/chatgpt-import/TOOLS.json +4 -0
  56. package/src/config/bundled-skills/computer-use/TOOLS.json +50 -2
  57. package/src/config/bundled-skills/contacts/SKILL.md +42 -35
  58. package/src/config/bundled-skills/contacts/TOOLS.json +22 -2
  59. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +38 -58
  60. package/src/config/bundled-skills/contacts/tools/contact-search.ts +11 -31
  61. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +19 -37
  62. package/src/config/bundled-skills/document/TOOLS.json +8 -0
  63. package/src/config/bundled-skills/email-setup/SKILL.md +10 -7
  64. package/src/config/bundled-skills/followups/TOOLS.json +12 -0
  65. package/src/config/bundled-skills/google-calendar/TOOLS.json +124 -26
  66. package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +54 -21
  67. package/src/config/bundled-skills/image-studio/TOOLS.json +12 -2
  68. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +14 -8
  69. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +13 -3
  70. package/src/config/bundled-skills/media-processing/SKILL.md +1 -1
  71. package/src/config/bundled-skills/media-processing/TOOLS.json +28 -0
  72. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +26 -6
  73. package/src/config/bundled-skills/messaging/TOOLS.json +228 -182
  74. package/src/config/bundled-skills/notifications/SKILL.md +3 -2
  75. package/src/config/bundled-skills/notifications/TOOLS.json +7 -13
  76. package/src/config/bundled-skills/phone-calls/TOOLS.json +13 -1
  77. package/src/config/bundled-skills/playbooks/TOOLS.json +16 -0
  78. package/src/config/bundled-skills/reminder/TOOLS.json +15 -2
  79. package/src/config/bundled-skills/schedule/SKILL.md +33 -15
  80. package/src/config/bundled-skills/schedule/TOOLS.json +17 -1
  81. package/src/config/bundled-skills/slack/SKILL.md +30 -1
  82. package/src/config/bundled-skills/slack/TOOLS.json +89 -2
  83. package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +146 -0
  84. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +120 -0
  85. package/src/config/bundled-skills/slack-app-setup/SKILL.md +200 -0
  86. package/src/config/bundled-skills/subagent/TOOLS.json +22 -2
  87. package/src/config/bundled-skills/tasks/TOOLS.json +86 -14
  88. package/src/config/bundled-skills/transcribe/TOOLS.json +4 -0
  89. package/src/config/bundled-skills/watcher/TOOLS.json +20 -0
  90. package/src/config/bundled-skills/weather/TOOLS.json +4 -0
  91. package/src/config/bundled-tool-registry.ts +2 -0
  92. package/src/config/channel-permission-profiles.ts +155 -0
  93. package/src/config/env.ts +4 -1
  94. package/src/contacts/contact-store.ts +195 -4
  95. package/src/contacts/types.ts +26 -0
  96. package/src/daemon/assistant-attachments.ts +23 -3
  97. package/src/daemon/guardian-verification-intent.ts +7 -4
  98. package/src/daemon/handlers/apps.ts +1 -2
  99. package/src/daemon/handlers/config-inbox.ts +16 -134
  100. package/src/daemon/handlers/guardian-actions.ts +20 -87
  101. package/src/daemon/handlers/sessions.ts +0 -1
  102. package/src/daemon/ipc-contract/apps.ts +0 -1
  103. package/src/daemon/ipc-contract/inbox.ts +7 -66
  104. package/src/daemon/ipc-contract/sessions.ts +1 -0
  105. package/src/daemon/ipc-contract/surfaces.ts +0 -1
  106. package/src/daemon/ipc-contract-inventory.json +2 -4
  107. package/src/daemon/lifecycle.ts +14 -2
  108. package/src/daemon/session-agent-loop-handlers.ts +9 -0
  109. package/src/daemon/session-agent-loop.ts +1 -0
  110. package/src/daemon/session-attachments.ts +5 -1
  111. package/src/daemon/session-error.ts +18 -0
  112. package/src/daemon/session-lifecycle.ts +4 -5
  113. package/src/daemon/session-media-retry.ts +15 -1
  114. package/src/daemon/session-surfaces.ts +0 -1
  115. package/src/daemon/session-tool-setup.ts +7 -4
  116. package/src/events/domain-events.ts +2 -1
  117. package/src/home-base/prebuilt/seed.ts +0 -1
  118. package/src/influencer/client.ts +7 -24
  119. package/src/media/gemini-image-service.ts +48 -3
  120. package/src/memory/app-store.ts +0 -4
  121. package/src/memory/conversation-attention-store.ts +3 -1
  122. package/src/memory/db-init.ts +4 -0
  123. package/src/memory/migrations/133-assistant-contact-metadata.ts +21 -0
  124. package/src/memory/migrations/index.ts +1 -0
  125. package/src/memory/schema.ts +12 -0
  126. package/src/memory/slack-thread-store.ts +187 -0
  127. package/src/messaging/providers/slack/client.ts +84 -26
  128. package/src/messaging/providers/slack/types.ts +4 -0
  129. package/src/notifications/adapters/slack.ts +90 -0
  130. package/src/notifications/destination-resolver.ts +42 -1
  131. package/src/notifications/emit-signal.ts +17 -1
  132. package/src/oauth/provider-profiles.ts +22 -0
  133. package/src/providers/anthropic/client.ts +3 -0
  134. package/src/providers/openai/client.ts +3 -0
  135. package/src/providers/retry.ts +9 -1
  136. package/src/runtime/actor-trust-resolver.ts +8 -0
  137. package/src/runtime/auth/require-bound-guardian.ts +44 -0
  138. package/src/runtime/auth/route-policy.ts +4 -8
  139. package/src/runtime/channel-approval-types.ts +18 -0
  140. package/src/runtime/channel-approvals.ts +8 -0
  141. package/src/runtime/channel-invite-transport.ts +1 -1
  142. package/src/runtime/channel-reply-delivery.ts +62 -3
  143. package/src/runtime/gateway-client.ts +36 -2
  144. package/src/runtime/gateway-internal-client.ts +86 -0
  145. package/src/runtime/guardian-action-service.ts +127 -0
  146. package/src/runtime/guardian-verification-templates.ts +16 -1
  147. package/src/runtime/http-server.ts +20 -49
  148. package/src/runtime/invite-redemption-service.ts +1 -1
  149. package/src/runtime/{ingress-service.ts → invite-service.ts} +5 -157
  150. package/src/runtime/nl-approval-parser.ts +138 -0
  151. package/src/runtime/routes/approval-routes.ts +1 -40
  152. package/src/runtime/routes/channel-route-shared.ts +35 -1
  153. package/src/runtime/routes/contact-routes.ts +196 -28
  154. package/src/runtime/routes/guardian-action-routes.ts +19 -111
  155. package/src/runtime/routes/guardian-approval-interception.ts +76 -0
  156. package/src/runtime/routes/inbound-message-handler.ts +40 -12
  157. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +222 -0
  158. package/src/runtime/routes/inbound-stages/background-dispatch.ts +108 -0
  159. package/src/runtime/routes/{ingress-routes.ts → invite-routes.ts} +10 -110
  160. package/src/runtime/slack-block-formatting.ts +176 -0
  161. package/src/schedule/scheduler.ts +11 -2
  162. package/src/tools/apps/executors.ts +16 -15
  163. package/src/tools/calls/call-end.ts +1 -1
  164. package/src/tools/computer-use/definitions.ts +16 -0
  165. package/src/tools/credentials/vault.ts +86 -2
  166. package/src/tools/network/script-proxy/session-manager.ts +28 -3
  167. package/src/tools/permission-checker.ts +18 -0
  168. package/src/tools/terminal/shell.ts +15 -5
  169. package/src/tools/tool-approval-handler.ts +48 -4
  170. package/src/tools/types.ts +38 -1
  171. package/src/util/errors.ts +5 -1
  172. package/src/util/retry.ts +21 -0
  173. package/src/watcher/providers/slack.ts +33 -3
  174. /package/src/memory/{ingress-invite-store.ts → invite-store.ts} +0 -0
@@ -0,0 +1,147 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { createContextSummaryMessage } from "../context/window-manager.js";
4
+ import { stripMediaPayloadsForRetry } from "../daemon/session-media-retry.js";
5
+ import type { ContentBlock, Message } from "../providers/types.js";
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Helpers
9
+ // ---------------------------------------------------------------------------
10
+
11
+ function makeImageBlock(
12
+ data = "AAAA",
13
+ mediaType = "image/png",
14
+ ): Extract<ContentBlock, { type: "image" }> {
15
+ return {
16
+ type: "image",
17
+ source: { type: "base64", media_type: mediaType, data },
18
+ };
19
+ }
20
+
21
+ function makeUserMessage(...blocks: ContentBlock[]): Message {
22
+ return { role: "user", content: blocks };
23
+ }
24
+
25
+ function makeAssistantMessage(...blocks: ContentBlock[]): Message {
26
+ return { role: "assistant", content: blocks };
27
+ }
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Tests
31
+ // ---------------------------------------------------------------------------
32
+
33
+ describe("stripMediaPayloadsForRetry", () => {
34
+ test("keeps images in the latest user message", () => {
35
+ const img = makeImageBlock();
36
+ const messages: Message[] = [
37
+ makeUserMessage({ type: "text", text: "hello" }, img),
38
+ ];
39
+
40
+ const result = stripMediaPayloadsForRetry(messages);
41
+ expect(result.modified).toBe(false);
42
+ expect(result.replacedBlocks).toBe(0);
43
+ });
44
+
45
+ test("strips images from older user messages", () => {
46
+ const messages: Message[] = [
47
+ makeUserMessage({ type: "text", text: "old" }, makeImageBlock()),
48
+ makeAssistantMessage({ type: "text", text: "response" }),
49
+ makeUserMessage({ type: "text", text: "new" }),
50
+ ];
51
+
52
+ const result = stripMediaPayloadsForRetry(messages);
53
+ expect(result.modified).toBe(true);
54
+ expect(result.replacedBlocks).toBe(1);
55
+ // The image in the first message should be replaced with a text stub
56
+ const firstMsg = result.messages[0];
57
+ expect(firstMsg.content[1].type).toBe("text");
58
+ expect(
59
+ (firstMsg.content[1] as { type: "text"; text: string }).text,
60
+ ).toContain("Image omitted");
61
+ });
62
+
63
+ test("keeps images in summary message when it is the only user message", () => {
64
+ // This is the bug scenario: forced compaction with minKeepRecentUserTurns: 0
65
+ // compacts everything, preserving images into the summary message. Media
66
+ // stubbing should NOT strip those images since there's no other user message.
67
+ const img = makeImageBlock();
68
+ const summaryMsg = createContextSummaryMessage(
69
+ "Prior conversation summary",
70
+ );
71
+ summaryMsg.content.push(
72
+ {
73
+ type: "text",
74
+ text: "[The following images were uploaded by the user in earlier messages and are preserved for reference.]",
75
+ },
76
+ img,
77
+ );
78
+
79
+ const messages: Message[] = [summaryMsg];
80
+
81
+ const result = stripMediaPayloadsForRetry(messages);
82
+ expect(result.modified).toBe(false);
83
+ expect(result.replacedBlocks).toBe(0);
84
+ // The image should still be present
85
+ const imageBlocks = result.messages[0].content.filter(
86
+ (b) => b.type === "image",
87
+ );
88
+ expect(imageBlocks.length).toBe(1);
89
+ });
90
+
91
+ test("keeps images in summary message when only other user messages are tool-result-only", () => {
92
+ const img = makeImageBlock();
93
+ const summaryMsg = createContextSummaryMessage("Summary");
94
+ summaryMsg.content.push(img);
95
+
96
+ const toolResultMsg: Message = {
97
+ role: "user",
98
+ content: [
99
+ {
100
+ type: "tool_result",
101
+ tool_use_id: "tool-1",
102
+ content: "result text",
103
+ },
104
+ ],
105
+ };
106
+
107
+ const messages: Message[] = [
108
+ summaryMsg,
109
+ makeAssistantMessage({ type: "text", text: "ok" }),
110
+ toolResultMsg,
111
+ ];
112
+
113
+ const result = stripMediaPayloadsForRetry(messages);
114
+ expect(result.modified).toBe(false);
115
+ expect(result.replacedBlocks).toBe(0);
116
+ });
117
+
118
+ test("prefers non-summary user message over summary when both exist", () => {
119
+ const summaryImg = makeImageBlock("BBBB");
120
+ const summaryMsg = createContextSummaryMessage("Summary");
121
+ summaryMsg.content.push(summaryImg);
122
+
123
+ const userImg = makeImageBlock("CCCC");
124
+ const userMsg = makeUserMessage({ type: "text", text: "latest" }, userImg);
125
+
126
+ const messages: Message[] = [
127
+ summaryMsg,
128
+ makeAssistantMessage({ type: "text", text: "response" }),
129
+ userMsg,
130
+ ];
131
+
132
+ const result = stripMediaPayloadsForRetry(messages);
133
+ // Summary image should be stripped, user image should be kept
134
+ expect(result.modified).toBe(true);
135
+ expect(result.replacedBlocks).toBe(1);
136
+
137
+ // Summary message image → stubbed
138
+ const summaryBlocks = result.messages[0].content;
139
+ const summaryImageBlocks = summaryBlocks.filter((b) => b.type === "image");
140
+ expect(summaryImageBlocks.length).toBe(0);
141
+
142
+ // Latest user message image → kept
143
+ const userBlocks = result.messages[2].content;
144
+ const userImageBlocks = userBlocks.filter((b) => b.type === "image");
145
+ expect(userImageBlocks.length).toBe(1);
146
+ });
147
+ });
@@ -141,17 +141,20 @@ describe("buildSystemPrompt feature flag filtering", () => {
141
141
 
142
142
  currentConfig = {
143
143
  sandbox: { enabled: false, backend: "native" },
144
- assistantFeatureFlagValues: { [DECLARED_FLAG_KEY]: false },
144
+ assistantFeatureFlagValues: {
145
+ [DECLARED_FLAG_KEY]: false,
146
+ "feature_flags.twitter.enabled": true,
147
+ },
145
148
  };
146
149
 
147
150
  const result = buildSystemPrompt();
148
151
 
149
- // twitter should be visible, declared flagged skill should not
152
+ // twitter is explicitly enabled, declared flagged skill is explicitly off
150
153
  expect(result).toContain('id="twitter"');
151
154
  expect(result).not.toContain(`id="${DECLARED_SKILL_ID}"`);
152
155
  });
153
156
 
154
- test("all skills visible when featureFlags is empty", () => {
157
+ test("declared skills hidden when featureFlags is empty (registry defaults to false)", () => {
155
158
  createSkillOnDisk(
156
159
  DECLARED_SKILL_ID,
157
160
  "Hatch New Assistant",
@@ -166,8 +169,9 @@ describe("buildSystemPrompt feature flag filtering", () => {
166
169
 
167
170
  const result = buildSystemPrompt();
168
171
 
169
- expect(result).toContain(`id="${DECLARED_SKILL_ID}"`);
170
- expect(result).toContain('id="twitter"');
172
+ // Both skills are declared in the registry with defaultEnabled: false
173
+ expect(result).not.toContain(`id="${DECLARED_SKILL_ID}"`);
174
+ expect(result).not.toContain('id="twitter"');
171
175
  });
172
176
 
173
177
  test("flagged-off skills hidden even when all workspace skill flags are OFF", () => {
@@ -59,9 +59,9 @@ function makeSkill(
59
59
  // ---------------------------------------------------------------------------
60
60
 
61
61
  describe("isSkillFeatureEnabled", () => {
62
- test("returns true when no flag overrides", () => {
62
+ test("returns false when no flag overrides (registry default is false)", () => {
63
63
  const config = makeConfig();
64
- expect(isSkillFeatureEnabled(DECLARED_SKILL_ID, config)).toBe(true);
64
+ expect(isSkillFeatureEnabled(DECLARED_SKILL_ID, config)).toBe(false);
65
65
  });
66
66
 
67
67
  test("returns true when skill key is explicitly true", () => {
@@ -103,8 +103,10 @@ describe("isAssistantFeatureFlagEnabled", () => {
103
103
 
104
104
  test("falls back to registry default when no override", () => {
105
105
  const config = makeConfig();
106
- // hatch-new-assistant defaults to true in the registry
107
- expect(isAssistantFeatureFlagEnabled(DECLARED_FLAG_KEY, config)).toBe(true);
106
+ // hatch-new-assistant defaults to false in the registry
107
+ expect(isAssistantFeatureFlagEnabled(DECLARED_FLAG_KEY, config)).toBe(
108
+ false,
109
+ );
108
110
  });
109
111
 
110
112
  test("respects persisted overrides for undeclared keys", () => {
@@ -116,11 +118,12 @@ describe("isAssistantFeatureFlagEnabled", () => {
116
118
  ).toBe(false);
117
119
  });
118
120
 
119
- test("undeclared keys with no persisted override default to enabled", () => {
121
+ test("declared keys with no persisted override use registry default", () => {
120
122
  const config = makeConfig();
123
+ // browser is declared in the registry with defaultEnabled: false
121
124
  expect(
122
125
  isAssistantFeatureFlagEnabled("feature_flags.browser.enabled", config),
123
- ).toBe(true);
126
+ ).toBe(false);
124
127
  });
125
128
  });
126
129
 
@@ -132,7 +135,10 @@ describe("resolveSkillStates with feature flags", () => {
132
135
  test("flag OFF skill does not appear in resolved list", () => {
133
136
  const catalog = [makeSkill(DECLARED_SKILL_ID), makeSkill("twitter")];
134
137
  const config = makeConfig({
135
- assistantFeatureFlagValues: { [DECLARED_FLAG_KEY]: false },
138
+ assistantFeatureFlagValues: {
139
+ [DECLARED_FLAG_KEY]: false,
140
+ "feature_flags.twitter.enabled": true,
141
+ },
136
142
  });
137
143
 
138
144
  const resolved = resolveSkillStates(catalog, config);
@@ -158,13 +164,13 @@ describe("resolveSkillStates with feature flags", () => {
158
164
  expect(ids).toContain("twitter");
159
165
  });
160
166
 
161
- test("missing flag key defaults to enabled", () => {
167
+ test("declared flag key defaults to registry value (false)", () => {
162
168
  const catalog = [makeSkill(DECLARED_SKILL_ID)];
163
169
  const config = makeConfig();
164
170
 
165
171
  const resolved = resolveSkillStates(catalog, config);
166
- expect(resolved.length).toBe(1);
167
- expect(resolved[0].summary.id).toBe(DECLARED_SKILL_ID);
172
+ // hatch-new-assistant registry default is false, so it's filtered out
173
+ expect(resolved.length).toBe(0);
168
174
  });
169
175
 
170
176
  test("feature flag OFF takes precedence over user-enabled config entry", () => {
@@ -202,6 +208,7 @@ describe("resolveSkillStates with feature flags", () => {
202
208
  const config = makeConfig({
203
209
  assistantFeatureFlagValues: {
204
210
  [DECLARED_FLAG_KEY]: false,
211
+ "feature_flags.twitter.enabled": true,
205
212
  "feature_flags.deploy.enabled": false,
206
213
  },
207
214
  });
@@ -209,8 +216,7 @@ describe("resolveSkillStates with feature flags", () => {
209
216
  const resolved = resolveSkillStates(catalog, config);
210
217
  const ids = resolved.map((r) => r.summary.id);
211
218
 
212
- // Both declared (hatch-new-assistant) and undeclared (deploy) skills with
213
- // persisted false overrides are filtered out; only twitter remains.
219
+ // hatch-new-assistant and deploy explicitly false; twitter explicitly true
214
220
  expect(ids).toEqual(["twitter"]);
215
221
  });
216
222
  });
@@ -154,7 +154,7 @@ describe("skill_load feature flag enforcement", () => {
154
154
  expect(result.content).toContain("Skill: Hatch New Assistant");
155
155
  });
156
156
 
157
- test("loads skill normally when flag key is absent (defaults to enabled)", async () => {
157
+ test("rejects skill when flag key is absent (registry defaults to disabled)", async () => {
158
158
  writeSkill(
159
159
  DECLARED_SKILL_ID,
160
160
  "Hatch New Assistant",
@@ -172,7 +172,8 @@ describe("skill_load feature flag enforcement", () => {
172
172
 
173
173
  const result = await executeSkillLoad({ skill: DECLARED_SKILL_ID });
174
174
 
175
- expect(result.isError).toBe(false);
176
- expect(result.content).toContain("Skill: Hatch New Assistant");
175
+ // hatch-new-assistant is declared in the registry with defaultEnabled: false
176
+ expect(result.isError).toBe(true);
177
+ expect(result.content).toContain("disabled by feature flag");
177
178
  });
178
179
  });
@@ -0,0 +1,100 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ isSlackCallbackUrl,
5
+ textToSlackBlocks,
6
+ } from "../runtime/slack-block-formatting.js";
7
+
8
+ describe("textToSlackBlocks", () => {
9
+ test("returns undefined for empty text", () => {
10
+ expect(textToSlackBlocks("")).toBeUndefined();
11
+ expect(textToSlackBlocks(" ")).toBeUndefined();
12
+ });
13
+
14
+ test("converts plain text to a single section block", () => {
15
+ const blocks = textToSlackBlocks("Hello, world!");
16
+ expect(blocks).toEqual([
17
+ { type: "section", text: { type: "mrkdwn", text: "Hello, world!" } },
18
+ ]);
19
+ });
20
+
21
+ test("converts heading to header block", () => {
22
+ const blocks = textToSlackBlocks("# Title\n\nBody text.");
23
+ expect(blocks).toBeDefined();
24
+ expect(blocks![0]).toEqual({
25
+ type: "header",
26
+ text: { type: "plain_text", text: "Title" },
27
+ });
28
+ });
29
+
30
+ test("wraps fenced code in triple backticks", () => {
31
+ const blocks = textToSlackBlocks("```ts\nconst x = 1;\n```");
32
+ expect(blocks).toBeDefined();
33
+ expect(blocks![0]).toEqual({
34
+ type: "section",
35
+ text: { type: "mrkdwn", text: "```ts\nconst x = 1;\n```" },
36
+ });
37
+ });
38
+
39
+ test("converts markdown links to Slack format", () => {
40
+ const blocks = textToSlackBlocks("See [docs](https://example.com).");
41
+ expect(blocks).toBeDefined();
42
+ expect(blocks![0].type).toBe("section");
43
+ const sectionBlock = blocks![0] as {
44
+ type: "section";
45
+ text: { type: string; text: string };
46
+ };
47
+ expect(sectionBlock.text.text).toBe("See <https://example.com|docs>.");
48
+ });
49
+
50
+ test("converts **bold** to *bold*", () => {
51
+ const blocks = textToSlackBlocks("**important**");
52
+ expect(blocks).toBeDefined();
53
+ const sectionBlock = blocks![0] as {
54
+ type: "section";
55
+ text: { type: string; text: string };
56
+ };
57
+ expect(sectionBlock.text.text).toBe("*important*");
58
+ });
59
+
60
+ test("inserts dividers between segments", () => {
61
+ const blocks = textToSlackBlocks("# Heading\n\nParagraph.");
62
+ expect(blocks).toBeDefined();
63
+ const types = blocks!.map((b) => b.type);
64
+ expect(types).toContain("divider");
65
+ });
66
+ });
67
+
68
+ describe("isSlackCallbackUrl", () => {
69
+ test("returns true for Slack deliver URLs", () => {
70
+ expect(
71
+ isSlackCallbackUrl(
72
+ "http://127.0.0.1:7830/deliver/slack?threadTs=123&channel=C456",
73
+ ),
74
+ ).toBe(true);
75
+ });
76
+
77
+ test("returns true for bare Slack deliver path", () => {
78
+ expect(isSlackCallbackUrl("http://localhost:7830/deliver/slack")).toBe(
79
+ true,
80
+ );
81
+ });
82
+
83
+ test("returns false for non-Slack URLs", () => {
84
+ expect(isSlackCallbackUrl("http://localhost:7830/deliver/telegram")).toBe(
85
+ false,
86
+ );
87
+ });
88
+
89
+ test("returns false for invalid URLs", () => {
90
+ expect(isSlackCallbackUrl("not-a-url")).toBe(false);
91
+ });
92
+
93
+ test("returns false for managed outbound URLs", () => {
94
+ expect(
95
+ isSlackCallbackUrl(
96
+ "http://localhost:7830/v1/internal/managed-gateway/outbound-send/?route_id=r1&assistant_id=a1&source_channel=sms",
97
+ ),
98
+ ).toBe(false);
99
+ });
100
+ });