@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.
- package/ARCHITECTURE.md +39 -37
- package/README.md +5 -6
- package/docs/runbook-trusted-contacts.md +79 -43
- package/package.json +1 -1
- package/scripts/ipc/check-swift-decoder-drift.ts +2 -3
- package/scripts/test.sh +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +4 -37
- package/src/__tests__/actor-token-service.test.ts +4 -3
- package/src/__tests__/app-executors.test.ts +7 -17
- package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -10
- package/src/__tests__/browser-skill-endstate.test.ts +10 -1
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -0
- package/src/__tests__/channel-approval-routes.test.ts +44 -44
- package/src/__tests__/channel-approval.test.ts +8 -0
- package/src/__tests__/channel-approvals.test.ts +39 -1
- package/src/__tests__/channel-guardian.test.ts +15 -5
- package/src/__tests__/channel-reply-delivery.test.ts +31 -0
- package/src/__tests__/commit-message-enrichment-service.test.ts +4 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +9 -0
- package/src/__tests__/gateway-only-guard.test.ts +1 -0
- package/src/__tests__/gemini-image-service.test.ts +2 -2
- package/src/__tests__/guardian-grant-minting.test.ts +6 -6
- package/src/__tests__/guardian-routing-invariants.test.ts +34 -11
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +4 -6
- package/src/__tests__/inbound-invite-redemption.test.ts +1 -1
- package/src/__tests__/integrations-cli.test.ts +3 -27
- package/src/__tests__/intent-routing.test.ts +3 -0
- package/src/__tests__/invite-redemption-service.test.ts +1 -1
- package/src/__tests__/{ingress-routes-http.test.ts → invite-routes-http.test.ts} +40 -320
- package/src/__tests__/ipc-snapshot.test.ts +4 -31
- package/src/__tests__/nl-approval-parser.test.ts +305 -0
- package/src/__tests__/oauth-provider-profiles.test.ts +34 -0
- package/src/__tests__/provider-error-scenarios.test.ts +68 -0
- package/src/__tests__/relay-server.test.ts +1 -1
- package/src/__tests__/retry-after-extraction.test.ts +111 -0
- package/src/__tests__/script-proxy-profile-template-fallback.test.ts +127 -0
- package/src/__tests__/session-media-retry.test.ts +147 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +9 -5
- package/src/__tests__/skill-feature-flags.test.ts +18 -12
- package/src/__tests__/skill-load-feature-flag.test.ts +4 -3
- package/src/__tests__/slack-block-formatting.test.ts +100 -0
- package/src/__tests__/slack-inbound-verification.test.ts +346 -0
- package/src/__tests__/slack-reaction-approvals.test.ts +77 -0
- package/src/__tests__/slack-skill.test.ts +3 -2
- package/src/__tests__/starter-task-flow.test.ts +0 -1
- package/src/__tests__/trusted-contact-verification.test.ts +3 -1
- package/src/__tests__/voice-invite-redemption.test.ts +1 -1
- package/src/amazon/client.ts +7 -24
- package/src/calls/relay-server.ts +39 -11
- package/src/channels/config.ts +1 -1
- package/src/cli/integrations.ts +10 -66
- package/src/config/bundled-skills/app-builder/SKILL.md +193 -1500
- package/src/config/bundled-skills/app-builder/TOOLS.json +70 -18
- package/src/config/bundled-skills/browser/TOOLS.json +59 -2
- package/src/config/bundled-skills/chatgpt-import/TOOLS.json +4 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +50 -2
- package/src/config/bundled-skills/contacts/SKILL.md +42 -35
- package/src/config/bundled-skills/contacts/TOOLS.json +22 -2
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +38 -58
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +11 -31
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +19 -37
- package/src/config/bundled-skills/document/TOOLS.json +8 -0
- package/src/config/bundled-skills/email-setup/SKILL.md +10 -7
- package/src/config/bundled-skills/followups/TOOLS.json +12 -0
- package/src/config/bundled-skills/google-calendar/TOOLS.json +124 -26
- package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +54 -21
- package/src/config/bundled-skills/image-studio/TOOLS.json +12 -2
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +14 -8
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +13 -3
- package/src/config/bundled-skills/media-processing/SKILL.md +1 -1
- package/src/config/bundled-skills/media-processing/TOOLS.json +28 -0
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +26 -6
- package/src/config/bundled-skills/messaging/TOOLS.json +228 -182
- package/src/config/bundled-skills/notifications/SKILL.md +3 -2
- package/src/config/bundled-skills/notifications/TOOLS.json +7 -13
- package/src/config/bundled-skills/phone-calls/TOOLS.json +13 -1
- package/src/config/bundled-skills/playbooks/TOOLS.json +16 -0
- package/src/config/bundled-skills/reminder/TOOLS.json +15 -2
- package/src/config/bundled-skills/schedule/SKILL.md +33 -15
- package/src/config/bundled-skills/schedule/TOOLS.json +17 -1
- package/src/config/bundled-skills/slack/SKILL.md +30 -1
- package/src/config/bundled-skills/slack/TOOLS.json +89 -2
- package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +146 -0
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +120 -0
- package/src/config/bundled-skills/slack-app-setup/SKILL.md +200 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +22 -2
- package/src/config/bundled-skills/tasks/TOOLS.json +86 -14
- package/src/config/bundled-skills/transcribe/TOOLS.json +4 -0
- package/src/config/bundled-skills/watcher/TOOLS.json +20 -0
- package/src/config/bundled-skills/weather/TOOLS.json +4 -0
- package/src/config/bundled-tool-registry.ts +2 -0
- package/src/config/channel-permission-profiles.ts +155 -0
- package/src/config/env.ts +4 -1
- package/src/contacts/contact-store.ts +195 -4
- package/src/contacts/types.ts +26 -0
- package/src/daemon/assistant-attachments.ts +23 -3
- package/src/daemon/guardian-verification-intent.ts +7 -4
- package/src/daemon/handlers/apps.ts +1 -2
- package/src/daemon/handlers/config-inbox.ts +16 -134
- package/src/daemon/handlers/guardian-actions.ts +20 -87
- package/src/daemon/handlers/sessions.ts +0 -1
- package/src/daemon/ipc-contract/apps.ts +0 -1
- package/src/daemon/ipc-contract/inbox.ts +7 -66
- package/src/daemon/ipc-contract/sessions.ts +1 -0
- package/src/daemon/ipc-contract/surfaces.ts +0 -1
- package/src/daemon/ipc-contract-inventory.json +2 -4
- package/src/daemon/lifecycle.ts +14 -2
- package/src/daemon/session-agent-loop-handlers.ts +9 -0
- package/src/daemon/session-agent-loop.ts +1 -0
- package/src/daemon/session-attachments.ts +5 -1
- package/src/daemon/session-error.ts +18 -0
- package/src/daemon/session-lifecycle.ts +4 -5
- package/src/daemon/session-media-retry.ts +15 -1
- package/src/daemon/session-surfaces.ts +0 -1
- package/src/daemon/session-tool-setup.ts +7 -4
- package/src/events/domain-events.ts +2 -1
- package/src/home-base/prebuilt/seed.ts +0 -1
- package/src/influencer/client.ts +7 -24
- package/src/media/gemini-image-service.ts +48 -3
- package/src/memory/app-store.ts +0 -4
- package/src/memory/conversation-attention-store.ts +3 -1
- package/src/memory/db-init.ts +4 -0
- package/src/memory/migrations/133-assistant-contact-metadata.ts +21 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/schema.ts +12 -0
- package/src/memory/slack-thread-store.ts +187 -0
- package/src/messaging/providers/slack/client.ts +84 -26
- package/src/messaging/providers/slack/types.ts +4 -0
- package/src/notifications/adapters/slack.ts +90 -0
- package/src/notifications/destination-resolver.ts +42 -1
- package/src/notifications/emit-signal.ts +17 -1
- package/src/oauth/provider-profiles.ts +22 -0
- package/src/providers/anthropic/client.ts +3 -0
- package/src/providers/openai/client.ts +3 -0
- package/src/providers/retry.ts +9 -1
- package/src/runtime/actor-trust-resolver.ts +8 -0
- package/src/runtime/auth/require-bound-guardian.ts +44 -0
- package/src/runtime/auth/route-policy.ts +4 -8
- package/src/runtime/channel-approval-types.ts +18 -0
- package/src/runtime/channel-approvals.ts +8 -0
- package/src/runtime/channel-invite-transport.ts +1 -1
- package/src/runtime/channel-reply-delivery.ts +62 -3
- package/src/runtime/gateway-client.ts +36 -2
- package/src/runtime/gateway-internal-client.ts +86 -0
- package/src/runtime/guardian-action-service.ts +127 -0
- package/src/runtime/guardian-verification-templates.ts +16 -1
- package/src/runtime/http-server.ts +20 -49
- package/src/runtime/invite-redemption-service.ts +1 -1
- package/src/runtime/{ingress-service.ts → invite-service.ts} +5 -157
- package/src/runtime/nl-approval-parser.ts +138 -0
- package/src/runtime/routes/approval-routes.ts +1 -40
- package/src/runtime/routes/channel-route-shared.ts +35 -1
- package/src/runtime/routes/contact-routes.ts +196 -28
- package/src/runtime/routes/guardian-action-routes.ts +19 -111
- package/src/runtime/routes/guardian-approval-interception.ts +76 -0
- package/src/runtime/routes/inbound-message-handler.ts +40 -12
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +222 -0
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +108 -0
- package/src/runtime/routes/{ingress-routes.ts → invite-routes.ts} +10 -110
- package/src/runtime/slack-block-formatting.ts +176 -0
- package/src/schedule/scheduler.ts +11 -2
- package/src/tools/apps/executors.ts +16 -15
- package/src/tools/calls/call-end.ts +1 -1
- package/src/tools/computer-use/definitions.ts +16 -0
- package/src/tools/credentials/vault.ts +86 -2
- package/src/tools/network/script-proxy/session-manager.ts +28 -3
- package/src/tools/permission-checker.ts +18 -0
- package/src/tools/terminal/shell.ts +15 -5
- package/src/tools/tool-approval-handler.ts +48 -4
- package/src/tools/types.ts +38 -1
- package/src/util/errors.ts +5 -1
- package/src/util/retry.ts +21 -0
- package/src/watcher/providers/slack.ts +33 -3
- /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: {
|
|
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
|
|
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("
|
|
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
|
-
|
|
170
|
-
expect(result).toContain(
|
|
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
|
|
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(
|
|
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
|
|
107
|
-
expect(isAssistantFeatureFlagEnabled(DECLARED_FLAG_KEY, config)).toBe(
|
|
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("
|
|
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(
|
|
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: {
|
|
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("
|
|
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
|
-
|
|
167
|
-
expect(resolved
|
|
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
|
-
//
|
|
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("
|
|
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
|
-
|
|
176
|
-
expect(result.
|
|
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
|
+
});
|