@desplega.ai/agent-swarm 2.4.4 → 2.4.5
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/dist/{acp-adapter-thq04adh.js → acp-adapter-2wdkktv6.js} +1 -1
- package/dist/{actions-j6ywh6x5.js → actions-c40ebcm4.js} +18 -18
- package/dist/{amp-adapter-k8b5xmj8.js → amp-adapter-wb67m95z.js} +2 -2
- package/dist/{app-rb7vc02s.js → app-353vre8g.js} +3 -3
- package/dist/{assistant-ftta1zjf.js → assistant-sj1njh23.js} +21 -21
- package/dist/{boot-reembed-srp6v6af.js → boot-reembed-b9c3az3c.js} +1 -1
- package/dist/{boot-reembed-xns7h5h9.js → boot-reembed-qdkvxeem.js} +1 -1
- package/dist/{boot-scrub-sweep-3qqrh2mz.js → boot-scrub-sweep-m3wh8eks.js} +2 -2
- package/dist/{boot-seeds-9ks8hce7.js → boot-seeds-j2273s54.js} +4 -4
- package/dist/{claude-adapter-k5ksyres.js → claude-adapter-m7q0a085.js} +2 -2
- package/dist/{claude-managed-adapter-h2y889ge.js → claude-managed-adapter-5ptb6tzs.js} +2 -2
- package/dist/{claude-managed-setup-n5a5eqks.js → claude-managed-setup-smrqk5j5.js} +3 -3
- package/dist/{claude-sdk-session-2j3j4kwn.js → claude-sdk-session-38xnz21e.js} +8284 -8267
- package/dist/{cli-cynvc9fj.js → cli-018r4jq6.js} +5 -5
- package/dist/{cli-9fhkkpj5.js → cli-1f3qzw6y.js} +1 -1
- package/dist/{cli-dqb4n629.js → cli-1sp861s4.js} +7 -7
- package/dist/{cli-gqvh8d8a.js → cli-1syn2xy1.js} +10 -10
- package/dist/{cli-qkhc5aqy.js → cli-37j04k05.js} +11 -11
- package/dist/{cli-1d70crsy.js → cli-3a45zcgm.js} +1 -1
- package/dist/{cli-t7cyef5g.js → cli-43x64hv3.js} +4 -4
- package/dist/{cli-hj3debgw.js → cli-4zfqgjc5.js} +2 -2
- package/dist/{cli-55qmbp6z.js → cli-7c9qs530.js} +1 -1
- package/dist/{cli-9qtjfd60.js → cli-848hxdwj.js} +1 -1
- package/dist/{cli-pccfmtb5.js → cli-94gwyg7m.js} +7 -7
- package/dist/{cli-51y09c6p.js → cli-9hstj09h.js} +5 -5
- package/dist/{cli-zrwpctp4.js → cli-9yqgm9nd.js} +22 -22
- package/dist/{cli-esxerzg7.js → cli-a92r1127.js} +1 -1
- package/dist/{cli-b8k7epj1.js → cli-be2161h1.js} +7 -7
- package/dist/{cli-xc27fwzg.js → cli-bnes4z6b.js} +2 -2
- package/dist/{cli-zrk6jxv8.js → cli-cxxe61f6.js} +1 -1
- package/dist/{cli-amyhm3gj.js → cli-dk44dxec.js} +1 -1
- package/dist/{cli-nc35pxb7.js → cli-eww120g8.js} +1 -0
- package/dist/{cli-a0kcty51.js → cli-f01h25yt.js} +1 -1
- package/dist/{cli-82d3nkc3.js → cli-f0g3pgvf.js} +1 -1
- package/dist/{cli-1ephgwnq.js → cli-ftc1g5tj.js} +2 -2
- package/dist/{cli-2v045xyf.js → cli-fv5hepyy.js} +3 -3
- package/dist/{cli-bnz0r3kc.js → cli-g35m2xwm.js} +4 -4
- package/dist/{cli-qje9k2nh.js → cli-g9ypd5en.js} +12 -2
- package/dist/{cli-w6nadkh4.js → cli-j99azg3g.js} +3 -3
- package/dist/{cli-er83cjbn.js → cli-k8g16gz3.js} +416 -116
- package/dist/{cli-4dg55wsk.js → cli-kpw04va0.js} +1 -1
- package/dist/{cli-3cgngyd8.js → cli-ms85309d.js} +33 -15
- package/dist/{cli-v3yr2gbg.js → cli-n72rg53p.js} +1471 -635
- package/dist/{cli-37yb7307.js → cli-p14m2cnp.js} +1 -1
- package/dist/{cli-2zqpkvtt.js → cli-qfxc84gh.js} +1 -1
- package/dist/{cli-44159k28.js → cli-qwhy2qma.js} +1 -1
- package/dist/{cli-2y91wnbh.js → cli-st8aw1m6.js} +221 -9
- package/dist/{cli-vq97g1jq.js → cli-tw4p90jb.js} +92 -192
- package/dist/{cli-te6b5wvg.js → cli-v0yrfmdh.js} +1 -1
- package/dist/{cli-xj0re5tm.js → cli-v8fjakwf.js} +1 -1
- package/dist/{cli-p20r8w42.js → cli-wt0jf9hm.js} +215 -80
- package/dist/{cli-q6r7t299.js → cli-xpa64rek.js} +33 -11
- package/dist/{cli-vdm20wej.js → cli-z7d29rw9.js} +2 -2
- package/dist/cli.js +19 -19
- package/dist/{codex-adapter-968xwfy7.js → codex-adapter-tn6k5mkg.js} +1 -1
- package/dist/{codex-session-runner-ht4z99fa.js → codex-session-runner-7vjt7jbw.js} +1 -1
- package/dist/{commands-pcymaeg9.js → commands-t7rz9146.js} +2 -2
- package/dist/{controller-fpjyk46r.js → controller-m2jasma5.js} +2 -2
- package/dist/{controllers-connect-mwzx1fh6.js → controllers-connect-9ftpex99.js} +19 -19
- package/dist/{cursor-adapter-cp4s2e2f.js → cursor-adapter-mjv79e4z.js} +1 -1
- package/dist/{db-xsjg9jqq.js → db-6sfrn9g3.js} +1 -1
- package/dist/{db-sgd12bt9.js → db-rn673r06.js} +3 -3
- package/dist/{dispatcher-x0jfw2c9.js → dispatcher-c155xz0p.js} +17 -17
- package/dist/{dsh-adapter-jrq7ma7h.js → dsh-adapter-3mmszc1x.js} +1 -1
- package/dist/{external-channel-8njj4h3b.js → external-channel-35k6tkv8.js} +2 -2
- package/dist/{grok-adapter-kkr97v2f.js → grok-adapter-tfrq7tg1.js} +2 -2
- package/dist/handlers-7gw64dxw.js +39 -0
- package/dist/{heartbeat-wd03zhn0.js → heartbeat-dyg4npkp.js} +20 -20
- package/dist/{hook-f0vghj42.js → hook-drspbwpe.js} +1 -1
- package/dist/{hook-xzj2ts2p.js → hook-x26b31bg.js} +1 -1
- package/dist/{http-4hwxv62p.js → http-pk972xyw.js} +355 -216
- package/dist/{index-ww2vznkp.js → index-arytgj5q.js} +1 -1
- package/dist/{index-hg0x2cdv.js → index-bgdyvcnv.js} +17 -17
- package/dist/{index-c4ykd0f1.js → index-dr94zbnh.js} +13348 -11611
- package/dist/{index-gdh8jrdm.js → index-gabbcdn2.js} +17 -17
- package/dist/{index-97q4xn03.js → index-jwxkkex2.js} +129 -75
- package/dist/index-tfb26zhy.js +41 -0
- package/dist/{keepalive-hrh7xnr9.js → keepalive-7yrng7px.js} +3 -3
- package/dist/{lead-ee8vazz2.js → lead-7e02z839.js} +29 -29
- package/dist/{maintenance-e8wnwnqx.js → maintenance-e6cemdqt.js} +1 -1
- package/dist/{node-3v3f08f7.js → node-de6akczr.js} +10 -9
- package/dist/{oauth-refresh-sweep-zm0yj4sj.js → oauth-refresh-sweep-yw3xdbcj.js} +2 -2
- package/dist/{onboard-dtwcq603.js → onboard-0a9y5yrc.js} +2 -2
- package/dist/{otel-impl-w3fbhpn0.js → otel-impl-trjpkfb7.js} +1 -1
- package/dist/{pricing-refresh-j38dehfs.js → pricing-refresh-d2b5my0y.js} +3 -3
- package/dist/{rbac-roles-ejxx9b35.js → rbac-roles-hey3yv9d.js} +2 -2
- package/dist/{rbac-roles-ae8yttq4.js → rbac-roles-nxh5ahwk.js} +1 -1
- package/dist/{registry-rxw10xzg.js → registry-ajhexe44.js} +1 -1
- package/dist/{render-v2-n16n0phe.js → render-v2-rr3p2atg.js} +3 -3
- package/dist/{resume-z0fgjc38.js → resume-2efkf1s1.js} +5 -5
- package/dist/{session-access-cv3mq6mc.js → session-access-s2mttxpw.js} +4 -4
- package/dist/{setup-mafhj3yt.js → setup-vypma5yt.js} +1 -1
- package/dist/{work-objects-8b86mgjp.js → work-objects-rvhckdkp.js} +20 -20
- package/dist/{worker-16s75hm5.js → worker-5snytpbd.js} +29 -29
- package/openapi.json +9 -2
- package/package.json +7 -7
- package/src/apps/catalog.generated.json +206 -4
- package/src/apps/page-validator.ts +13 -1
- package/src/be/chunking.ts +42 -9
- package/src/be/db/tasks/draft-text.ts +15 -0
- package/src/be/db/tasks/read.ts +14 -1
- package/src/be/db.ts +5 -1
- package/src/be/memory/index-content.ts +36 -17
- package/src/be/memory/providers/openai-embedding.ts +115 -61
- package/src/be/memory/providers/sqlite-store.ts +69 -23
- package/src/be/memory/reranker.ts +31 -1
- package/src/be/memory/search-candidates.ts +47 -0
- package/src/be/memory/types.ts +14 -0
- package/src/be/scripts/typecheck.ts +2 -0
- package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +106 -54
- package/src/be/seed-scripts/index.ts +2 -2
- package/src/be/swarm-config-guard.ts +24 -1
- package/src/be/task-attachment-store.ts +30 -0
- package/src/extensions/catalog.generated.json +4 -4
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +7 -0
- package/src/fs/provider.ts +13 -1
- package/src/heartbeat/heartbeat.ts +31 -11
- package/src/http/fs.ts +54 -34
- package/src/http/mcp-body.ts +37 -0
- package/src/http/mcp-user.ts +3 -5
- package/src/http/mcp.ts +6 -7
- package/src/http/memory.ts +26 -15
- package/src/http/page-proxy.ts +61 -5
- package/src/http/pages-public.ts +27 -44
- package/src/http/pages.ts +4 -4
- package/src/http/script-runs.ts +2 -0
- package/src/http/users.ts +12 -1
- package/src/integrations/chat-sdk/adapters.ts +7 -3
- package/src/integrations/chat-sdk/attachments.ts +90 -12
- package/src/integrations/chat-sdk/facts.ts +120 -0
- package/src/integrations/chat-sdk/inbound.ts +156 -8
- package/src/integrations/chat-sdk/limits.ts +24 -0
- package/src/integrations/chat-sdk/outbound-files.ts +186 -0
- package/src/integrations/chat-sdk/outbound.ts +234 -4
- package/src/integrations/chat-sdk/relay.ts +4 -1
- package/src/integrations/chat-sdk/router.ts +28 -2
- package/src/integrations/chat-sdk/steering.ts +9 -0
- package/src/integrations/chat-sdk/templates.ts +14 -0
- package/src/integrations/chat-sdk/thread-buffer.ts +4 -0
- package/src/mcp-limits.ts +20 -0
- package/src/pages/browser-sdk.ts +8 -3
- package/src/pages/feedback-overlay.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +106 -119
- package/src/providers/codex-agents-md.ts +35 -229
- package/src/providers/cursor-adapter.ts +1 -1
- package/src/realtime/transport.ts +1 -2
- package/src/script-workflows/workflow-ctx.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +2 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +8 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +2 -0
- package/src/server-user.ts +2 -0
- package/src/server.ts +2 -0
- package/src/slack/assistant.ts +2 -2
- package/src/slack/files.ts +2 -0
- package/src/slack/handlers.ts +4 -4
- package/src/slack/inbound-files.ts +39 -6
- package/src/slack/thread-buffer.ts +2 -1
- package/src/tasks/task-terminal-effects.ts +21 -17
- package/src/tests/apps-card-grid.test.ts +197 -0
- package/src/tests/apps-catalog-additions.test.ts +120 -0
- package/src/tests/apps-tree.test.ts +129 -0
- package/src/tests/chat-sdk-attachments-reply.test.ts +52 -0
- package/src/tests/chat-sdk-config.test.ts +11 -0
- package/src/tests/chat-sdk-facts.test.ts +23 -0
- package/src/tests/chat-sdk-inbound.test.ts +558 -4
- package/src/tests/chat-sdk-outbound-files.test.ts +652 -0
- package/src/tests/codex-adapter.test.ts +167 -242
- package/src/tests/config-api-only-keys.test.ts +9 -0
- package/src/tests/cors-allowlist.test.ts +1 -1
- package/src/tests/extensions-example-slack-reply-gate.test.ts +262 -91
- package/src/tests/heartbeat-checklist.test.ts +33 -0
- package/src/tests/heartbeat-jev-gate-credentials.test.ts +44 -0
- package/src/tests/heartbeat-jev-gate.test.ts +242 -1
- package/src/tests/kv-page-proxy.test.ts +91 -5
- package/src/tests/launch-password-rejection.test.ts +2 -2
- package/src/tests/mcp-input-caps.test.ts +294 -0
- package/src/tests/memory-chunked-search-starvation.test.ts +200 -0
- package/src/tests/memory-embedding-input-cap.test.ts +114 -0
- package/src/tests/memory.test.ts +14 -0
- package/src/tests/page-guest-session.test.ts +19 -8
- package/src/tests/page-proxy-authed.test.ts +1 -1
- package/src/tests/page-proxy.test.ts +17 -10
- package/src/tests/page-session.test.ts +102 -1
- package/src/tests/pages-authed-mode.test.ts +82 -12
- package/src/tests/pages-password-mode.test.ts +5 -5
- package/src/tests/realtime-transport.test.ts +13 -7
- package/src/tests/script-runs-http.test.ts +84 -0
- package/src/tests/scripts-http.test.ts +10 -1
- package/src/tests/swarm-config-export-to-env.test.ts +22 -3
- package/src/tests/task-audio-transcription.test.ts +299 -0
- package/src/tests/task-completion-memory-chunking.test.ts +434 -0
- package/src/tests/task-terminal-secret-scrub.test.ts +6 -1
- package/src/tests/transcription.test.ts +299 -0
- package/src/tests/workflow-system-one-decision-providers.test.ts +98 -1
- package/src/tests/workflow-system-one-decision.test.ts +2 -1
- package/src/tools/chat-access.ts +10 -0
- package/src/tools/chat-post.ts +31 -11
- package/src/tools/chat-reply.ts +32 -10
- package/src/tools/memory-search.ts +3 -1
- package/src/tools/system-one-decide.ts +17 -4
- package/src/types.ts +6 -0
- package/src/utils/config-env-export.ts +3 -1
- package/src/utils/page-session.ts +133 -20
- package/src/utils/transcription.ts +246 -0
- package/src/workflows/executors/system-one-decision.ts +28 -7
- package/src/workflows/executors/system-one-providers.ts +39 -4
- package/templates/extensions/slack-reply-gate/README.md +24 -22
- package/templates/extensions/slack-reply-gate/hooks.ts +146 -95
- package/templates/extensions/slack-reply-gate/manifest.json +2 -2
- package/templates/skills/apps/SKILL.md +28 -6
- package/templates/skills/apps/content.md +28 -6
- package/templates/skills/chat-sdk/content.md +6 -2
- package/templates/skills/code-quality/content.md +2 -1
- package/templates/skills/heartbeat-runbook/content.md +5 -5
- package/templates/skills/pages/content.md +6 -4
- package/templates/skills/swarm-scripts/SKILL.md +2 -2
- package/templates/skills/swarm-scripts/content.md +2 -2
- package/dist/handlers-0pvee9nw.js +0 -39
- package/dist/index-thm0y6a8.js +0 -41
package/src/server.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { isSteeringEnabled } from "./be/steering";
|
|
|
7
7
|
import { registerGithubTaskReactions } from "./github/task-reactions";
|
|
8
8
|
import { loadGlobalConfigsIntoEnv } from "./http/core";
|
|
9
9
|
import { isChatSdkEnabled } from "./integrations/chat-sdk/config";
|
|
10
|
+
import { MCP_MAX_TOOL_INPUT_ELEMENTS } from "./mcp-limits";
|
|
10
11
|
import { resolveTemplate } from "./prompts/resolver";
|
|
11
12
|
import { isRbacEnabled } from "./rbac";
|
|
12
13
|
import { getSlackConfiguration } from "./slack/config";
|
|
@@ -450,6 +451,7 @@ export async function createServer(
|
|
|
450
451
|
capabilities: {
|
|
451
452
|
logging: {},
|
|
452
453
|
},
|
|
454
|
+
maxToolInputElements: MCP_MAX_TOOL_INPUT_ELEMENTS,
|
|
453
455
|
},
|
|
454
456
|
);
|
|
455
457
|
|
package/src/slack/assistant.ts
CHANGED
|
@@ -204,13 +204,13 @@ export function createAssistant(): Assistant {
|
|
|
204
204
|
// Fetch shared files before the task text is final: a file that can't be
|
|
205
205
|
// downloaded is flagged in it.
|
|
206
206
|
if (files?.length) await safeSetStatus("Downloading attachments...");
|
|
207
|
-
await using inbound = await fetchSlackFiles(client, files);
|
|
207
|
+
await using inbound = await fetchSlackFiles(client, files, { transcribe: true });
|
|
208
208
|
// Any in-body `<@U…>` mention the requester typed is rewritten via
|
|
209
209
|
// the identity primitive before it reaches agent-visible task text —
|
|
210
210
|
// never a raw Slack ID. Bot-mention routing checks above use the
|
|
211
211
|
// raw `messageText`, not this rendered copy.
|
|
212
212
|
const renderedMessageText = await rewriteSlackMentions(
|
|
213
|
-
buildEffectiveText(messageText, inbound.files, inbound.failed),
|
|
213
|
+
buildEffectiveText(messageText, inbound.files, inbound.failed, inbound.notes),
|
|
214
214
|
cachedBotUserId ?? undefined,
|
|
215
215
|
);
|
|
216
216
|
|
package/src/slack/files.ts
CHANGED
package/src/slack/handlers.ts
CHANGED
|
@@ -702,8 +702,8 @@ export function registerMessageHandler(app: App): void {
|
|
|
702
702
|
return ignoreSlackInbound("rate_limited");
|
|
703
703
|
}
|
|
704
704
|
|
|
705
|
-
await using inbound = await fetchSlackFiles(client, msg.files);
|
|
706
|
-
const taskText = buildEffectiveText(msg.text, inbound.files, inbound.failed);
|
|
705
|
+
await using inbound = await fetchSlackFiles(client, msg.files, { transcribe: true });
|
|
706
|
+
const taskText = buildEffectiveText(msg.text, inbound.files, inbound.failed, inbound.notes);
|
|
707
707
|
const taskDescription = isImplicitMention
|
|
708
708
|
? taskText
|
|
709
709
|
: extractTaskFromMessage(taskText, botUserId);
|
|
@@ -778,9 +778,9 @@ export function registerMessageHandler(app: App): void {
|
|
|
778
778
|
}
|
|
779
779
|
|
|
780
780
|
// Extract task description (the text plus a `[File: …]` line per attachment)
|
|
781
|
-
await using inbound = await fetchSlackFiles(client, msg.files);
|
|
781
|
+
await using inbound = await fetchSlackFiles(client, msg.files, { transcribe: true });
|
|
782
782
|
const taskDescription = extractTaskFromMessage(
|
|
783
|
-
buildEffectiveText(msg.text, inbound.files, inbound.failed),
|
|
783
|
+
buildEffectiveText(msg.text, inbound.files, inbound.failed, inbound.notes),
|
|
784
784
|
botUserId,
|
|
785
785
|
);
|
|
786
786
|
if (!taskDescription) {
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* could not be attached — never dropped silently. A download failure is
|
|
14
14
|
* also flagged on that line; a storage failure happens after the text is
|
|
15
15
|
* written, so it shows up only in the reply and in the missing attachment.
|
|
16
|
+
* Audio files (voice clips) a user shares get their transcript on the next
|
|
17
|
+
* line when transcription is configured, and keep the audio attached.
|
|
16
18
|
*/
|
|
17
19
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
18
20
|
import { tmpdir } from "node:os";
|
|
@@ -27,6 +29,7 @@ import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
|
|
|
27
29
|
import type { AgentTask, CreateTaskOptions, TaskAttachment } from "../types";
|
|
28
30
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
29
31
|
import { taskAttachmentFetchCommand } from "../utils/task-attachment-links";
|
|
32
|
+
import { transcribeAudioNotes } from "../utils/transcription";
|
|
30
33
|
import { getFileInfo, type SlackFile } from "./files";
|
|
31
34
|
import { noteSlackInboundSideEffect } from "./inbound-dispatch";
|
|
32
35
|
// Side-effect import: registers all Slack event templates in the in-memory registry
|
|
@@ -73,6 +76,8 @@ export type InboundSlackFiles = {
|
|
|
73
76
|
files: SlackFile[];
|
|
74
77
|
fetched: FetchedSlackFile[];
|
|
75
78
|
failed: SlackFileFailure[];
|
|
79
|
+
/** Task-text note per audio file id (a transcript), filled when fetched with `transcribe`. */
|
|
80
|
+
notes?: Map<string, string>;
|
|
76
81
|
/** Deletes the downloaded copies; hold the batch with `await using`. */
|
|
77
82
|
[Symbol.asyncDispose](): Promise<void>;
|
|
78
83
|
};
|
|
@@ -96,9 +101,14 @@ export function formatFileSize(bytes: number): string {
|
|
|
96
101
|
* Build a text representation of file attachments for inclusion in messages.
|
|
97
102
|
* Each file is formatted as: [File: filename.ext (mimetype, size) id=FILE_ID],
|
|
98
103
|
* followed by `(not attached: <reason>)` when the file never made it into the
|
|
99
|
-
* task's attachments
|
|
104
|
+
* task's attachments, and by the file's note (a voice-clip transcript) on the
|
|
105
|
+
* next line.
|
|
100
106
|
*/
|
|
101
|
-
export function buildAttachmentText(
|
|
107
|
+
export function buildAttachmentText(
|
|
108
|
+
files: SlackFile[],
|
|
109
|
+
failed: SlackFileFailure[] = [],
|
|
110
|
+
notes: Map<string, string> = new Map(),
|
|
111
|
+
): string {
|
|
102
112
|
const reasons = new Map(failed.map((f) => [f.file.id, f.reason]));
|
|
103
113
|
return files
|
|
104
114
|
.map((f) => {
|
|
@@ -106,7 +116,8 @@ export function buildAttachmentText(files: SlackFile[], failed: SlackFileFailure
|
|
|
106
116
|
const size = typeof f.size === "number" ? formatFileSize(f.size) : "unknown size";
|
|
107
117
|
const line = `[File: ${f.name ?? f.id} (${f.mimetype ?? "unknown type"}, ${size}) id=${f.id}]`;
|
|
108
118
|
const reason = reasons.get(f.id);
|
|
109
|
-
|
|
119
|
+
const note = notes.get(f.id);
|
|
120
|
+
return [reason ? `${line} (not attached: ${reason})` : line, note].filter(Boolean).join("\n");
|
|
110
121
|
})
|
|
111
122
|
.join("\n");
|
|
112
123
|
}
|
|
@@ -121,15 +132,16 @@ export function buildEffectiveText(
|
|
|
121
132
|
text: string | undefined,
|
|
122
133
|
files?: SlackFile[],
|
|
123
134
|
failed?: SlackFileFailure[],
|
|
135
|
+
notes?: Map<string, string>,
|
|
124
136
|
): string {
|
|
125
137
|
const hasText = !!text?.trim();
|
|
126
138
|
const hasFiles = files && files.length > 0;
|
|
127
139
|
|
|
128
140
|
if (hasText && hasFiles) {
|
|
129
|
-
return `${text}\n\n${buildAttachmentText(files, failed)}`;
|
|
141
|
+
return `${text}\n\n${buildAttachmentText(files, failed, notes)}`;
|
|
130
142
|
}
|
|
131
143
|
if (hasFiles) {
|
|
132
|
-
return buildAttachmentText(files, failed);
|
|
144
|
+
return buildAttachmentText(files, failed, notes);
|
|
133
145
|
}
|
|
134
146
|
return text || "";
|
|
135
147
|
}
|
|
@@ -147,11 +159,14 @@ export function bufferedFileFailures(files: SlackFile[] | undefined): SlackFileF
|
|
|
147
159
|
* Download every file on a Slack message with the bot token, each to its own
|
|
148
160
|
* temp file. Never throws: a file that can't be fetched lands in `failed` with
|
|
149
161
|
* a reason the user and the agent can read. Hold the result with `await using`
|
|
150
|
-
* so the temp files go away with it.
|
|
162
|
+
* so the temp files go away with it. With `transcribe`, every downloaded audio
|
|
163
|
+
* file also gets a `notes` entry: its transcript, the failure, or a line saying
|
|
164
|
+
* no provider is configured.
|
|
151
165
|
*/
|
|
152
166
|
export async function fetchSlackFiles(
|
|
153
167
|
client: WebClient,
|
|
154
168
|
files: SlackFile[] | undefined,
|
|
169
|
+
options: { transcribe?: boolean } = {},
|
|
155
170
|
): Promise<InboundSlackFiles> {
|
|
156
171
|
let dir: string | undefined;
|
|
157
172
|
const result: InboundSlackFiles = {
|
|
@@ -182,9 +197,27 @@ export async function fetchSlackFiles(
|
|
|
182
197
|
result.fetched.push({ file, ...outcome });
|
|
183
198
|
}
|
|
184
199
|
}
|
|
200
|
+
if (options.transcribe) {
|
|
201
|
+
result.notes = await transcribeAudioNotes(
|
|
202
|
+
result.fetched
|
|
203
|
+
.filter(({ file }) => isSlackAudio(file))
|
|
204
|
+
.map(({ file, path }) => ({
|
|
205
|
+
key: file.id,
|
|
206
|
+
name: file.name ?? file.id,
|
|
207
|
+
data: Bun.file(path),
|
|
208
|
+
mimeType: file.mimetype,
|
|
209
|
+
})),
|
|
210
|
+
"[Slack]",
|
|
211
|
+
);
|
|
212
|
+
}
|
|
185
213
|
return result;
|
|
186
214
|
}
|
|
187
215
|
|
|
216
|
+
/** An audio file, or a voice clip recorded in Slack (its MIME type can read `video/…`). */
|
|
217
|
+
function isSlackAudio(file: SlackFile): boolean {
|
|
218
|
+
return file.subtype === "slack_audio" || Boolean(file.mimetype?.startsWith("audio/"));
|
|
219
|
+
}
|
|
220
|
+
|
|
188
221
|
/** Does the start of an HTML response read like Slack's sign-in page? */
|
|
189
222
|
function looksLikeSignIn(head: Uint8Array[]): boolean {
|
|
190
223
|
const joined = new Uint8Array(head.reduce((total, part) => total + part.byteLength, 0));
|
|
@@ -177,7 +177,7 @@ async function slackFlush(
|
|
|
177
177
|
const app = getSlackApp();
|
|
178
178
|
const files = items.flatMap((item) => item.files ?? []);
|
|
179
179
|
await using inbound = app
|
|
180
|
-
? await fetchSlackFiles(app.client, files)
|
|
180
|
+
? await fetchSlackFiles(app.client, files, { transcribe: true })
|
|
181
181
|
: {
|
|
182
182
|
files,
|
|
183
183
|
fetched: [],
|
|
@@ -196,6 +196,7 @@ async function slackFlush(
|
|
|
196
196
|
item.text,
|
|
197
197
|
item.files?.map((file) => resolvedFiles.get(file.id) ?? file),
|
|
198
198
|
inbound.failed,
|
|
199
|
+
inbound.notes,
|
|
199
200
|
),
|
|
200
201
|
)
|
|
201
202
|
.join("\n---\n"),
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { getSessionLogsByTaskId } from "@/be/db";
|
|
19
|
-
import {
|
|
19
|
+
import { indexMemoryContent, taskCompletionMemoryKey } from "@/be/memory/index-content";
|
|
20
20
|
import { getRetrievalsForTask } from "@/be/memory/raters/retrieval";
|
|
21
21
|
import { runServerRaters } from "@/be/memory/raters/run-server-raters";
|
|
22
22
|
import { shouldPersistTaskCompletionMemory } from "@/memory/automatic-task-gate";
|
|
@@ -55,21 +55,22 @@ export async function runTaskTerminalEffects(args: {
|
|
|
55
55
|
// Skip indexing if there's truly no content
|
|
56
56
|
if (taskContent.length < 30) return;
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
// Same path as memory-store: chunk, store, and embed each chunk, so a
|
|
59
|
+
// long output never meets the embedding model's input cap. The per-task
|
|
60
|
+
// key makes the chunks one document, like a keyed manual memory, and a
|
|
61
|
+
// repeat write for the task replaces that document atomically.
|
|
62
|
+
const key = taskCompletionMemoryKey(taskId);
|
|
63
|
+
await indexMemoryContent({
|
|
62
64
|
agentId: agentId ?? null,
|
|
63
65
|
content: taskContent,
|
|
64
66
|
name: `Task: ${safeTask.slice(0, 80)}`,
|
|
65
67
|
scope: "agent",
|
|
66
68
|
source: "task_completion",
|
|
67
69
|
sourceTaskId: taskId,
|
|
70
|
+
key,
|
|
71
|
+
intent: "task completion memory",
|
|
72
|
+
replaceExisting: true,
|
|
68
73
|
});
|
|
69
|
-
const embedding = await provider.embed(taskContent);
|
|
70
|
-
if (embedding) {
|
|
71
|
-
await store.updateEmbedding(memory.id, embedding, provider.name);
|
|
72
|
-
}
|
|
73
74
|
|
|
74
75
|
// Auto-promote high-value completions to swarm memory (P3)
|
|
75
76
|
const shouldShareWithSwarm =
|
|
@@ -80,24 +81,27 @@ export async function runTaskTerminalEffects(args: {
|
|
|
80
81
|
|
|
81
82
|
if (shouldShareWithSwarm) {
|
|
82
83
|
try {
|
|
83
|
-
|
|
84
|
+
await indexMemoryContent({
|
|
84
85
|
agentId: agentId ?? null,
|
|
85
|
-
scope: "swarm",
|
|
86
|
-
name: `Shared: ${safeTask.slice(0, 80)}`,
|
|
87
86
|
content: `Task completed by agent ${agentId}:\n\n${taskContent}`,
|
|
87
|
+
name: `Shared: ${safeTask.slice(0, 80)}`,
|
|
88
|
+
scope: "swarm",
|
|
88
89
|
source: "task_completion",
|
|
89
90
|
sourceTaskId: taskId,
|
|
91
|
+
key,
|
|
92
|
+
intent: "task completion memory",
|
|
93
|
+
replaceExisting: true,
|
|
90
94
|
});
|
|
91
|
-
const swarmEmbedding = await provider.embed(taskContent);
|
|
92
|
-
if (swarmEmbedding) {
|
|
93
|
-
await store.updateEmbedding(swarmMemory.id, swarmEmbedding, provider.name);
|
|
94
|
-
}
|
|
95
95
|
} catch {
|
|
96
96
|
// Non-blocking — swarm memory promotion failure is not critical
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
|
-
} catch {
|
|
99
|
+
} catch (err) {
|
|
100
100
|
// Non-blocking — task completion memory failure should not affect task status
|
|
101
|
+
console.error(
|
|
102
|
+
"[task-terminal-effects] task completion memory write failed:",
|
|
103
|
+
scrubSecrets(err instanceof Error ? err.message : String(err)),
|
|
104
|
+
);
|
|
101
105
|
}
|
|
102
106
|
})().catch((err) =>
|
|
103
107
|
console.error(
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { parseAppDefinition } from "../apps/definition";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Validator coverage for `CardGrid` and the Table column `format` template.
|
|
6
|
+
* Pure `parseAppDefinition` calls — no DB, no HTTP — mirroring
|
|
7
|
+
* `apps-catalog-additions.test.ts`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
type Element = { type: string; props?: Record<string, unknown>; children?: string[] };
|
|
11
|
+
|
|
12
|
+
function appWith(elements: Record<string, Element>) {
|
|
13
|
+
return {
|
|
14
|
+
models: {
|
|
15
|
+
doc: {
|
|
16
|
+
columns: {
|
|
17
|
+
title: { kind: "string", required: true },
|
|
18
|
+
author: { kind: "string" },
|
|
19
|
+
cover: { kind: "string" },
|
|
20
|
+
status: { kind: "enum", enum: ["draft", "published"], default: "draft" },
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
queries: {
|
|
25
|
+
docs: { model: "doc" },
|
|
26
|
+
docById: { model: "doc", filter: { id: { $param: "doc" } } },
|
|
27
|
+
},
|
|
28
|
+
pages: {
|
|
29
|
+
main: { title: "Main", root: "root", elements },
|
|
30
|
+
detail: {
|
|
31
|
+
title: "Detail",
|
|
32
|
+
params: { doc: { kind: "string" }, position: { kind: "number" } },
|
|
33
|
+
root: "detailRoot",
|
|
34
|
+
elements: { detailRoot: { type: "Text", props: { content: "Detail" } } },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
defaultPage: "main",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function issuesFor(input: unknown) {
|
|
42
|
+
const parsed = await parseAppDefinition(input);
|
|
43
|
+
return parsed.success ? [] : parsed.issues;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const el = (id: string) => `pages.main.elements.${id}`;
|
|
47
|
+
|
|
48
|
+
function cardGrid(props: Record<string, unknown>) {
|
|
49
|
+
return appWith({
|
|
50
|
+
root: { type: "Stack", props: {}, children: ["cards"] },
|
|
51
|
+
cards: {
|
|
52
|
+
type: "CardGrid",
|
|
53
|
+
props: {
|
|
54
|
+
data: { $state: "/queries/docs/data" },
|
|
55
|
+
loading: { $state: "/queries/docs/loading" },
|
|
56
|
+
error: { $state: "/queries/docs/error" },
|
|
57
|
+
titleKey: "title",
|
|
58
|
+
...props,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const navigateToDetail = [
|
|
65
|
+
{ action: "app.navigate", params: { page: "detail", params: { doc: { $row: "id" } } } },
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
describe("CardGrid", () => {
|
|
69
|
+
test("onClick running app.navigate to a declared page with $row params passes", async () => {
|
|
70
|
+
const issues = await issuesFor(
|
|
71
|
+
cardGrid({
|
|
72
|
+
imageKey: "cover",
|
|
73
|
+
fields: [
|
|
74
|
+
{ key: "author" },
|
|
75
|
+
{ key: "createdAt", kind: "date" },
|
|
76
|
+
{ key: "status", kind: "badge", tones: { published: "success" } },
|
|
77
|
+
],
|
|
78
|
+
columns: { base: 1, md: 2, lg: 4 },
|
|
79
|
+
emptyMessage: "No documents yet",
|
|
80
|
+
onClick: navigateToDetail,
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
if (issues.length > 0) console.error(issues);
|
|
84
|
+
expect(issues).toEqual([]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("$row and $rowIndex are accepted inside the onClick chain", async () => {
|
|
88
|
+
const issues = await issuesFor(
|
|
89
|
+
cardGrid({
|
|
90
|
+
onClick: [
|
|
91
|
+
{ action: "app.refresh", params: { query: "docs" } },
|
|
92
|
+
{
|
|
93
|
+
action: "app.navigate",
|
|
94
|
+
params: {
|
|
95
|
+
page: "detail",
|
|
96
|
+
params: { doc: { $row: "id" }, position: { $rowIndex: true } },
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
if (issues.length > 0) console.error(issues);
|
|
103
|
+
expect(issues).toEqual([]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("$row outside the onClick chain is rejected", async () => {
|
|
107
|
+
const issues = await issuesFor(cardGrid({ emptyMessage: { $row: "title" } }));
|
|
108
|
+
expect(issues).toContainEqual({
|
|
109
|
+
path: `${el("cards")}.props.emptyMessage`,
|
|
110
|
+
message: "$row, $rowIndex, $form, and $chain bindings are only allowed in action params",
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("onClick naming an unknown action fails", async () => {
|
|
115
|
+
const issues = await issuesFor(cardGrid({ onClick: [{ action: "app.nope", params: {} }] }));
|
|
116
|
+
expect(issues).toContainEqual({
|
|
117
|
+
path: `${el("cards")}.props.onClick.0.action`,
|
|
118
|
+
message: 'unknown action type "app.nope"',
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("onClick navigating to an undeclared page fails", async () => {
|
|
123
|
+
const issues = await issuesFor(
|
|
124
|
+
cardGrid({ onClick: [{ action: "app.navigate", params: { page: "nope" } }] }),
|
|
125
|
+
);
|
|
126
|
+
expect(issues).toContainEqual({
|
|
127
|
+
path: `${el("cards")}.props.onClick.0.params.page`,
|
|
128
|
+
message: 'unknown page "nope"',
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("titleKey is required", async () => {
|
|
133
|
+
const copy = cardGrid({});
|
|
134
|
+
delete copy.pages.main.elements.cards!.props!.titleKey;
|
|
135
|
+
const issues = await issuesFor(copy);
|
|
136
|
+
expect(issues.some((i) => i.path.startsWith(`${el("cards")}.props`))).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("data bound to a $state path passes", async () => {
|
|
140
|
+
const issues = await issuesFor(
|
|
141
|
+
appWith({
|
|
142
|
+
root: { type: "Stack", props: {}, children: ["cards"] },
|
|
143
|
+
cards: {
|
|
144
|
+
type: "CardGrid",
|
|
145
|
+
props: { data: { $state: "/queries/docs/data" }, titleKey: "title" },
|
|
146
|
+
},
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
if (issues.length > 0) console.error(issues);
|
|
150
|
+
expect(issues).toEqual([]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("an unknown field kind is rejected", async () => {
|
|
154
|
+
const issues = await issuesFor(cardGrid({ fields: [{ key: "author", kind: "status" }] }));
|
|
155
|
+
expect(issues.some((i) => i.path.startsWith(`${el("cards")}.props.fields.0.kind`))).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("Table column format", () => {
|
|
160
|
+
test("a column with a {{field}} format template passes", async () => {
|
|
161
|
+
const issues = await issuesFor(
|
|
162
|
+
appWith({
|
|
163
|
+
root: { type: "Stack", props: {}, children: ["table"] },
|
|
164
|
+
table: {
|
|
165
|
+
type: "Table",
|
|
166
|
+
props: {
|
|
167
|
+
data: { $state: "/queries/docs/data" },
|
|
168
|
+
columns: [
|
|
169
|
+
{ key: "title", label: "Document", format: "{{title}} ({{status}})" },
|
|
170
|
+
{ key: "author" },
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
}),
|
|
175
|
+
);
|
|
176
|
+
if (issues.length > 0) console.error(issues);
|
|
177
|
+
expect(issues).toEqual([]);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("format must be a string", async () => {
|
|
181
|
+
const issues = await issuesFor(
|
|
182
|
+
appWith({
|
|
183
|
+
root: { type: "Stack", props: {}, children: ["table"] },
|
|
184
|
+
table: {
|
|
185
|
+
type: "Table",
|
|
186
|
+
props: {
|
|
187
|
+
data: { $state: "/queries/docs/data" },
|
|
188
|
+
columns: [{ key: "title", format: 42 }],
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
}),
|
|
192
|
+
);
|
|
193
|
+
expect(issues.some((i) => i.path.startsWith(`${el("table")}.props.columns.0.format`))).toBe(
|
|
194
|
+
true,
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { parseAppDefinition } from "../apps/definition";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Validator coverage for the small catalog additions: `NumberInput`, Select
|
|
6
|
+
* `defaultValue`, and `fill` on Stack / Split. Pure `parseAppDefinition`
|
|
7
|
+
* calls — no DB, no HTTP — mirroring `apps-catalog-inputs.test.ts`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
type Element = { type: string; props?: Record<string, unknown>; children?: string[] };
|
|
11
|
+
|
|
12
|
+
function appWith(elements: Record<string, Element>) {
|
|
13
|
+
return {
|
|
14
|
+
models: {},
|
|
15
|
+
pages: { main: { title: "Main", root: "root", elements } },
|
|
16
|
+
defaultPage: "main",
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function issuesFor(input: unknown) {
|
|
21
|
+
const parsed = await parseAppDefinition(input);
|
|
22
|
+
return parsed.success ? [] : parsed.issues;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const el = (id: string) => `pages.main.elements.${id}`;
|
|
26
|
+
|
|
27
|
+
describe("NumberInput", () => {
|
|
28
|
+
test("a NumberInput's /ui/<id>/value read by a bound Text validates", async () => {
|
|
29
|
+
const issues = await issuesFor(
|
|
30
|
+
appWith({
|
|
31
|
+
root: { type: "Stack", props: {}, children: ["width", "echo"] },
|
|
32
|
+
width: {
|
|
33
|
+
type: "NumberInput",
|
|
34
|
+
props: { id: "x", label: "Width", placeholder: "px", min: 1, max: 4096 },
|
|
35
|
+
},
|
|
36
|
+
echo: { type: "Text", props: { content: { $state: "/ui/x/value" } } },
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
if (issues.length > 0) console.error(issues);
|
|
40
|
+
expect(issues).toEqual([]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("a /ui/<id>/value read with no UI control of that id is rejected", async () => {
|
|
44
|
+
const issues = await issuesFor(
|
|
45
|
+
appWith({
|
|
46
|
+
root: { type: "Stack", props: {}, children: ["echo"] },
|
|
47
|
+
echo: { type: "Text", props: { content: { $state: "/ui/x/value" } } },
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
expect(issues).toContainEqual({
|
|
51
|
+
path: `${el("echo")}.props.content`,
|
|
52
|
+
message: 'state reference targets unknown UI control "x"',
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("id is required and min/max must be numbers", async () => {
|
|
57
|
+
const missingId = await issuesFor(
|
|
58
|
+
appWith({
|
|
59
|
+
root: { type: "Stack", props: {}, children: ["width"] },
|
|
60
|
+
width: { type: "NumberInput", props: { label: "Width" } },
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
expect(missingId.map((issue) => issue.path)).toContain(`${el("width")}.props.id`);
|
|
64
|
+
|
|
65
|
+
const badBounds = await issuesFor(
|
|
66
|
+
appWith({
|
|
67
|
+
root: { type: "Stack", props: {}, children: ["width"] },
|
|
68
|
+
width: { type: "NumberInput", props: { id: "x", min: "1", max: "10" } },
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
const paths = badBounds.map((issue) => issue.path);
|
|
72
|
+
expect(paths).toContain(`${el("width")}.props.min`);
|
|
73
|
+
expect(paths).toContain(`${el("width")}.props.max`);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("Select defaultValue", () => {
|
|
78
|
+
const select = (props: Record<string, unknown>) =>
|
|
79
|
+
appWith({
|
|
80
|
+
root: { type: "Stack", props: {}, children: ["device", "echo"] },
|
|
81
|
+
device: {
|
|
82
|
+
type: "Select",
|
|
83
|
+
props: { id: "device", options: ["phone", "tablet", "desktop"], ...props },
|
|
84
|
+
},
|
|
85
|
+
echo: { type: "Text", props: { content: { $state: "/ui/device/value" } } },
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("a Select with defaultValue validates", async () => {
|
|
89
|
+
const issues = await issuesFor(select({ defaultValue: "tablet" }));
|
|
90
|
+
if (issues.length > 0) console.error(issues);
|
|
91
|
+
expect(issues).toEqual([]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("defaultValue must be a string", async () => {
|
|
95
|
+
const paths = (await issuesFor(select({ defaultValue: 2 }))).map((issue) => issue.path);
|
|
96
|
+
expect(paths).toContain(`${el("device")}.props.defaultValue`);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("Stack and Split fill", () => {
|
|
101
|
+
const layout = (stackFill: unknown, splitFill: unknown) =>
|
|
102
|
+
appWith({
|
|
103
|
+
root: { type: "Stack", props: { fill: stackFill }, children: ["split"] },
|
|
104
|
+
split: { type: "Split", props: { fill: splitFill, ratio: "1-1" }, children: ["a", "b"] },
|
|
105
|
+
a: { type: "Text", props: { content: "left" } },
|
|
106
|
+
b: { type: "Text", props: { content: "right" } },
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("fill: true validates on both", async () => {
|
|
110
|
+
const issues = await issuesFor(layout(true, true));
|
|
111
|
+
if (issues.length > 0) console.error(issues);
|
|
112
|
+
expect(issues).toEqual([]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("fill must be a boolean", async () => {
|
|
116
|
+
const paths = (await issuesFor(layout("yes", 1))).map((issue) => issue.path);
|
|
117
|
+
expect(paths).toContain(`${el("root")}.props.fill`);
|
|
118
|
+
expect(paths).toContain(`${el("split")}.props.fill`);
|
|
119
|
+
});
|
|
120
|
+
});
|