@synap-core/workspace-templates 0.12.0 → 0.23.0
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/README.md +483 -0
- package/automations/bookmark-enrichment.automation.json +321 -0
- package/automations/github-pr-events.automation.json +505 -0
- package/automations/mail-feed.automation.json +86 -0
- package/automations/ship-a-change-checks-green.automation.json +106 -0
- package/automations/ship-a-change-checks-red.automation.json +106 -0
- package/automations/ship-a-change-deployed.automation.json +56 -0
- package/automations/ship-a-change-pr-merged.automation.json +110 -0
- package/automations/ship-a-change-pr-opened.automation.json +104 -0
- package/automations/url-bookmark-capture.automation.json +132 -0
- package/capabilities/agency-skills.capability.json +157 -0
- package/capabilities/apify.capability.json +265 -0
- package/capabilities/apollo.capability.json +120 -0
- package/capabilities/arch-backend.capability.json +97 -0
- package/capabilities/arch-client-intelligence.capability.json +401 -0
- package/capabilities/backblaze-b2.capability.json +205 -0
- package/capabilities/cal-com.capability.json +266 -0
- package/capabilities/canva.capability.json +274 -0
- package/capabilities/claude-code-routines.capability.json +172 -0
- package/capabilities/claude-managed-agents.capability.json +215 -0
- package/capabilities/client-comms-report.capability.json +246 -0
- package/capabilities/content-idea-to-production.capability.json +29 -0
- package/capabilities/datagouv.capability.json +315 -0
- package/capabilities/discord-bot.capability.json +157 -0
- package/capabilities/dropbox.capability.json +281 -0
- package/capabilities/exa.capability.json +109 -0
- package/capabilities/fal-ai.capability.json +83 -0
- package/capabilities/fal-media-lab.capability.json +158 -0
- package/capabilities/fireflies.capability.json +191 -0
- package/capabilities/freellmapi.capability.json +114 -0
- package/capabilities/generic-apikey.capability.json +92 -0
- package/capabilities/github-merge.capability.json +99 -0
- package/capabilities/github-read.capability.json +325 -0
- package/capabilities/governance-tighten-recommender.capability.json +81 -0
- package/capabilities/jev.capability.json +226 -0
- package/capabilities/lead-conversion-enrollment.capability.json +246 -0
- package/capabilities/mail-feed.capability.json +277 -0
- package/capabilities/mailgun.capability.json +52 -0
- package/capabilities/nango-google.capability.json +639 -0
- package/capabilities/notion.capability.json +417 -0
- package/capabilities/omniroute.capability.json +137 -0
- package/capabilities/posthog-analytics.capability.json +258 -0
- package/capabilities/postiz.capability.json +467 -0
- package/capabilities/proactive-digest.capability.json +190 -0
- package/capabilities/proton.capability.json +288 -0
- package/capabilities/replicate-rvm.capability.json +97 -0
- package/capabilities/research-methods.capability.json +57 -0
- package/capabilities/shopify.capability.json +395 -0
- package/capabilities/slack.capability.json +369 -0
- package/capabilities/stripe.capability.json +420 -0
- package/capabilities/telegram-bridge.capability.json +56 -0
- package/capabilities/treg.capability.json +102 -0
- package/capabilities/unipile-linkedin.capability.json +471 -0
- package/capabilities/vercel.capability.json +424 -0
- package/dist/define-CPS0n8BV.d.ts +1331 -0
- package/dist/index.d.ts +174 -900
- package/dist/index.js +26148 -23931
- package/dist/index.js.map +1 -1
- package/dist/wire.d.ts +222 -0
- package/dist/wire.js +77 -0
- package/dist/wire.js.map +1 -0
- package/package.json +27 -4
- package/packs/bookmark-views-pack.json +33 -0
- package/packs/crm-pack.json +76 -0
- package/packs/customer-profile.json +28 -0
- package/packs/deep-research.playbook.json +30 -0
- package/packs/event-views-pack.json +33 -0
- package/packs/habit-profile.json +27 -0
- package/packs/hr-pack.json +54 -0
- package/packs/invoice-profile.json +28 -0
- package/packs/knowledge-base-pack.json +56 -0
- package/packs/meeting-profile.json +29 -0
- package/packs/product-profile.json +30 -0
- package/packs/project-management-pack.json +49 -0
- package/packs/recipe-profile.json +29 -0
- package/packs/task-views-pack.json +39 -0
- package/retired.yaml +136 -0
- package/skills/app-factory-build-slice/SKILL.md +70 -0
- package/skills/app-factory-build-slice.catalog.json +17 -0
- package/skills/app-factory-design-canvas/SKILL.md +96 -0
- package/skills/app-factory-design-canvas.catalog.json +17 -0
- package/skills/app-factory-design-review/SKILL.md +99 -0
- package/skills/app-factory-design-review.catalog.json +18 -0
- package/skills/app-factory-research/SKILL.md +123 -0
- package/skills/app-factory-research.catalog.json +17 -0
- package/skills/bookmark-enrichment/SKILL.md +40 -0
- package/skills/carousel-format/carousel-format.skill.json +18 -0
- package/skills/collage-broll/collage-broll.skill.json +20 -0
- package/skills/content-taste/content-taste.skill.json +18 -0
- package/skills/creative-director/creative-director.skill.json +18 -0
- package/skills/design-critique/SKILL.md +135 -0
- package/skills/design-critique.catalog.json +12 -0
- package/skills/designing-with-real-components/SKILL.md +140 -0
- package/skills/designing-with-real-components.catalog.json +12 -0
- package/skills/edit-loop/edit-loop.skill.json +25 -0
- package/skills/facecam-edit/facecam-edit.skill.json +24 -0
- package/skills/mapping-the-server/SKILL.md +54 -0
- package/skills/math-explainer-manimgx/math-explainer-manimgx.skill.json +20 -0
- package/skills/motion-quality-gates/motion-quality-gates.skill.json +20 -0
- package/skills/open-design-edit/open-design-edit.skill.json +24 -0
- package/skills/reading-client-progress/SKILL.md +65 -0
- package/skills/routing-to-channels/SKILL.md +54 -0
- package/skills/stellar-scf-grants-advisor/ASSOCIATE-CHEATSHEET.md +133 -0
- package/skills/stellar-scf-grants-advisor/ASSOCIATE-ONBOARDING-GUIDE.md +761 -0
- package/skills/stellar-scf-grants-advisor/CLIENT-FAQ.md +181 -0
- package/skills/stellar-scf-grants-advisor/PARTNER-HANDOVER.md +130 -0
- package/skills/stellar-scf-grants-advisor/SKILL.md +102 -0
- package/skills/stellar-scf-grants-advisor/examples/README.md +43 -0
- package/skills/stellar-scf-grants-advisor/examples/chat-summary-templates.md +158 -0
- package/skills/stellar-scf-grants-advisor/examples/corpus-comparable-rows.md +136 -0
- package/skills/stellar-scf-grants-advisor/examples/q10-team-rewrites.md +109 -0
- package/skills/stellar-scf-grants-advisor/examples/q2-description-rewrites.md +88 -0
- package/skills/stellar-scf-grants-advisor/examples/q6-integration-rewrites.md +107 -0
- package/skills/stellar-scf-grants-advisor/examples/tranche-structures.md +176 -0
- package/skills/stellar-scf-grants-advisor/reference/01-methodology.md +96 -0
- package/skills/stellar-scf-grants-advisor/reference/02-scoring-framework.md +199 -0
- package/skills/stellar-scf-grants-advisor/reference/03-rejection-patterns.md +241 -0
- package/skills/stellar-scf-grants-advisor/reference/04-winner-patterns.md +120 -0
- package/skills/stellar-scf-grants-advisor/reference/05-comeback-patterns.md +92 -0
- package/skills/stellar-scf-grants-advisor/reference/06-budget-benchmarks.md +125 -0
- package/skills/stellar-scf-grants-advisor/reference/07-integration-list.md +147 -0
- package/skills/stellar-scf-grants-advisor/reference/08-abstract-fields.md +258 -0
- package/skills/stellar-scf-grants-advisor/reference/09-build-fields.md +259 -0
- package/skills/stellar-scf-grants-advisor/reference/10-corpus-winners.md +135 -0
- package/skills/stellar-scf-grants-advisor/reference/11-corpus-rejects.md +267 -0
- package/skills/stellar-scf-grants-advisor/reference/12-client-communication.md +131 -0
- package/skills/stellar-scf-grants-advisor/reference/13-excel-output-spec.md +199 -0
- package/skills/stellar-scf-grants-advisor/reference/14-worked-examples.md +160 -0
- package/skills/stellar-scf-grants-advisor/templates/abstract-review-builder.py +253 -0
- package/skills/stellar-scf-grants-advisor/templates/build-review-builder.py +213 -0
- package/skills/video-direction/video-direction.skill.json +20 -0
- package/skills/video-style-vocabulary/video-style-vocabulary.skill.json +20 -0
- package/skills/video-toolbox/video-toolbox.skill.json +20 -0
- package/src/__fixtures__/lifecycle-presets.flat-before.json +205 -0
- package/src/agency-os.yaml +122 -0
- package/src/agent-os-skill-slugs.test.ts +65 -0
- package/src/apify-social-reader.test.ts +317 -0
- package/src/base.yaml +85 -39
- package/src/blockchain-ecosystem.yaml +1 -0
- package/src/brand-library.yaml +525 -14
- package/src/builder-dev-fold.template.test.ts +113 -0
- package/src/builder.yaml +779 -91
- package/src/business-model-catalog-description.test.ts +47 -0
- package/src/business-model-grp.template.test.ts +214 -0
- package/src/business-model.yaml +296 -49
- package/src/catalog-safety.test.ts +198 -0
- package/src/catalog.template-items.test.ts +75 -0
- package/src/catalog.ts +66 -2
- package/src/concept-guard.test.ts +28 -8
- package/src/concept-guard.ts +2 -2
- package/src/content-creation.template.test.ts +52 -0
- package/src/content-creation.yaml +192 -0
- package/src/content-os.edit-loop.template.test.ts +210 -0
- package/src/content-os.publish-due-posts.template.test.ts +261 -0
- package/src/content-os.template.test.ts +322 -0
- package/src/content-os.video-takes.template.test.ts +338 -0
- package/src/content-os.work-a-take.template.test.ts +125 -0
- package/src/content-os.yaml +1830 -39
- package/src/content-studio-retired.template.test.ts +82 -0
- package/src/corpus-flow-validity.test.ts +4 -5
- package/src/crm-absorbs-outreach.template.test.ts +117 -0
- package/src/crm-build-plan.template.test.ts +128 -0
- package/src/crm-lists.template.test.ts +96 -0
- package/src/crm-messages-cadence.template.test.ts +164 -0
- package/src/crm-one-send-door.template.test.ts +65 -0
- package/src/crm.yaml +1456 -181
- package/src/decisions-wave.template.test.ts +186 -0
- package/src/define.ts +130 -17
- package/src/design-to-implementation.template.test.ts +94 -0
- package/src/domain-template-type.test.ts +75 -0
- package/src/ecosystem.yaml +27 -66
- package/src/enterprise-os.yaml +10 -134
- package/src/facecam-edit.audio-studio.test.ts +188 -0
- package/src/fal-generate-image.test.ts +200 -0
- package/src/fal-media-lab-pod-files.test.ts +180 -0
- package/src/finance.yaml +254 -1
- package/src/foundation.yaml +78 -38
- package/src/frame-host-modules.d.mts +6 -0
- package/src/frame-host-modules.mjs +58 -0
- package/src/generate.mjs +9 -3
- package/src/github-merge.test.ts +210 -0
- package/src/grants.yaml +46 -17
- package/src/hr.yaml +39 -6
- package/src/index.ts +36 -1
- package/src/intent.test.ts +161 -0
- package/src/internal-runbook.yaml +19 -1
- package/src/jev.test.ts +356 -0
- package/src/legal.yaml +33 -1
- package/src/life-os.yaml +51 -240
- package/src/lifecycle-coverage.test.ts +124 -0
- package/src/lifecycle-declarations.test.ts +252 -0
- package/src/lifecycle-presets.test.ts +468 -0
- package/src/lifecycle-presets.ts +494 -0
- package/src/lifecycle-stuck.test.ts +46 -0
- package/src/lifecycle-waits.parity.test.ts +319 -0
- package/src/marketing.yaml +108 -4
- package/src/meetings-knowledge.yaml +18 -0
- package/src/networking.yaml +23 -2
- package/src/operations.yaml +304 -4
- package/src/pipeline-views.template.test.ts +88 -0
- package/src/playbook-goal-placeholders.test.ts +57 -0
- package/src/postiz-contract.test.ts +134 -0
- package/src/presentation-declarations.test.ts +174 -0
- package/src/process-contract.validate.test.ts +352 -0
- package/src/project-management.yaml +39 -740
- package/src/publish-body.d.mts +29 -0
- package/src/publish-body.mjs +74 -0
- package/src/publish-body.test.ts +74 -0
- package/src/publish-official-lib.test.ts +176 -0
- package/src/replicate-rvm.test.ts +504 -0
- package/src/research-base.yaml +119 -20
- package/src/research-decision.yaml +60 -3
- package/src/reserved-profile-slugs.test.ts +83 -0
- package/src/retired-goal-kinds.test.ts +58 -0
- package/src/role-reference-and-step-domain.test.ts +192 -0
- package/src/seed-obligation.test.ts +1 -6
- package/src/send-message-maps.test.ts +227 -0
- package/src/shared-role-ssot.test.ts +40 -2
- package/src/ship-a-change.template.test.ts +177 -0
- package/src/skills.test.ts +170 -0
- package/src/social.yaml +85 -27
- package/src/space-packages.freshness.test.ts +97 -0
- package/src/space-packages.generated.ts +40 -0
- package/src/space-packages.test.ts +125 -0
- package/src/space-packages.ts +73 -0
- package/src/start.test.ts +188 -0
- package/src/task-intents.test.ts +293 -0
- package/src/templates.ts +20851 -21496
- package/src/types.ts +443 -60
- package/src/unipile-publish-post.test.ts +162 -0
- package/src/validate-flow.drift.test.ts +2 -3
- package/src/validate-flow.ts +22 -2
- package/src/validate-onboarding.test.ts +59 -0
- package/src/validate.test.ts +71 -4
- package/src/validate.ts +1335 -7
- package/src/wire-core.ts +231 -0
- package/src/wire.test.ts +159 -0
- package/src/wire.ts +107 -0
- package/src/agent-fleet.yaml +0 -1645
- package/src/business-developer.template.test.ts +0 -351
- package/src/business-developer.yaml +0 -860
- package/src/content-studio.yaml +0 -361
- package/src/dev-dashboard.yaml +0 -990
- package/src/outreach-comms.yaml +0 -486
- package/src/the-arch.yaml +0 -201
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unipile — what a pod app may ask it to publish (intent actions, decision
|
|
3
|
+
* ef0e917c, 2026-10-28; the Postiz precedent is postiz-contract.test.ts).
|
|
4
|
+
*
|
|
5
|
+
* A pod app asks the person's tools for `publish_post` with ONE input
|
|
6
|
+
* `{ text, media?, link?, when? }`. Unipile reaches several accounts through one
|
|
7
|
+
* key, so `unipile_publish_post` names an account source and the pod offers one
|
|
8
|
+
* action per account: the shared `unipile_list_accounts`, every provider, so
|
|
9
|
+
* LinkedIn and Instagram each get their own action (founder, 28 Oct, on
|
|
10
|
+
* decision 34103d1b). The post's images and videos come as `file[]:bytes`
|
|
11
|
+
* (phase 2c of plan e7c3c98e): the code holds one handle per pod file and the
|
|
12
|
+
* pod streams the bytes into a multipart part, so Instagram can post.
|
|
13
|
+
*
|
|
14
|
+
* SEAM: the verb's REAL `code` body, run the way the pod's executor wraps it
|
|
15
|
+
* (`async (args, context) => { <code> }` with `callProvider` in scope), against
|
|
16
|
+
* a recorded `callProvider`.
|
|
17
|
+
*
|
|
18
|
+
* NOT covered: the pod's own checks of the map and of `file[]:bytes`
|
|
19
|
+
* (backend `intent-input.ts`, `file-parameters.ts`), the handles turning into
|
|
20
|
+
* bytes (backend `pod-file-body.ts`), and whether Unipile accepts the request
|
|
21
|
+
* (not exercised against a live Unipile).
|
|
22
|
+
*/
|
|
23
|
+
import { readFileSync } from "node:fs";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
import { describe, expect, it } from "vitest";
|
|
26
|
+
|
|
27
|
+
const capability = JSON.parse(
|
|
28
|
+
readFileSync(join(import.meta.dirname, "..", "capabilities", "unipile-linkedin.capability.json"), "utf8"),
|
|
29
|
+
) as { spec: { skills: Array<Record<string, any>>; provides?: string[] } };
|
|
30
|
+
|
|
31
|
+
const skill = (name: string) => capability.spec.skills.find((s) => s.name === name)!;
|
|
32
|
+
const verb = skill("unipile_publish_post");
|
|
33
|
+
|
|
34
|
+
const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor as new (
|
|
35
|
+
...args: string[]
|
|
36
|
+
) => (...a: unknown[]) => Promise<unknown>;
|
|
37
|
+
|
|
38
|
+
type Call = { tool: string; method: string; path: string; body: Record<string, unknown>; opts?: Record<string, unknown> };
|
|
39
|
+
async function run(args: Record<string, unknown>, reply: Record<string, unknown> = { status: 201, body: { id: "p1", share_url: "https://x/p1" } }) {
|
|
40
|
+
const calls: Call[] = [];
|
|
41
|
+
const callProvider = async (tool: string, method: string, path: string, body: Record<string, unknown>, opts?: Record<string, unknown>) => {
|
|
42
|
+
calls.push({ tool, method, path, body, opts });
|
|
43
|
+
return reply;
|
|
44
|
+
};
|
|
45
|
+
const body = new AsyncFunction("args", "context", "callProvider", "propose", verb.code);
|
|
46
|
+
const result = await body(args, {}, callProvider, {});
|
|
47
|
+
return { result, calls };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe("Unipile — what a pod app may ask it to publish", () => {
|
|
51
|
+
it("unipile_publish_post maps the text, the media and the account picked from unipile_list_accounts", () => {
|
|
52
|
+
expect(verb.intent).toBe("publish_post");
|
|
53
|
+
expect(verb.metadata).toEqual({ verbType: "write" });
|
|
54
|
+
expect(verb.inputMap).toEqual({
|
|
55
|
+
params: { text: "$.text", accountId: "$account.id", provider: "$account.provider", media: "$.media" },
|
|
56
|
+
accounts: { verb: "unipile_list_accounts", id: "id", label: "name", provider: "provider" },
|
|
57
|
+
});
|
|
58
|
+
expect(capability.spec.provides).toContain("publish_post");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("Unipile has no scheduling and links stay out: the map never reads when or link", () => {
|
|
62
|
+
const read = Object.values(verb.inputMap.params).filter((s) => !String(s).startsWith("$account."));
|
|
63
|
+
expect(read.sort()).toEqual(["$.media", "$.text"]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("the media are pod files sent as bytes, so the verb is code run in the pod, toward its own tool", () => {
|
|
67
|
+
expect(verb.kind).toBe("code");
|
|
68
|
+
expect(verb.executionMode).not.toBe("box");
|
|
69
|
+
expect(verb.parameters).toEqual({
|
|
70
|
+
accountId: "string",
|
|
71
|
+
text: "string",
|
|
72
|
+
provider: "string?",
|
|
73
|
+
media: "file[]:bytes?",
|
|
74
|
+
postAs: "string?",
|
|
75
|
+
quotedPostId: "string?",
|
|
76
|
+
});
|
|
77
|
+
expect(verb.requires).toEqual(["unipile_linkedin"]);
|
|
78
|
+
// An install-time value in the code would stop the pod updating Unipile.
|
|
79
|
+
expect(verb.code).not.toMatch(/\{\{\w+\}\}/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("text alone is a JSON post of exactly { text } to /{accountId}/posts", async () => {
|
|
83
|
+
const { result, calls } = await run({ accountId: "acc_1", text: "Hello", provider: "linkedin" });
|
|
84
|
+
expect(calls).toEqual([{ tool: "unipile_linkedin", method: "POST", path: "/acc_1/posts", body: { text: "Hello" }, opts: undefined }]);
|
|
85
|
+
expect(result).toEqual({ postId: "p1", shareUrl: "https://x/p1", attachments: 0 });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("with media, a multipart form: the text field and one attachments[i] part per pod file", async () => {
|
|
89
|
+
const { result, calls } = await run({ accountId: "acc_2", text: "Caption", provider: "instagram", media: ["podfile:a", "podfile:b"] });
|
|
90
|
+
expect(calls).toEqual([
|
|
91
|
+
{
|
|
92
|
+
tool: "unipile_linkedin",
|
|
93
|
+
method: "POST",
|
|
94
|
+
path: "/acc_2/posts",
|
|
95
|
+
body: { text: "Caption", "attachments[0]": "podfile:a", "attachments[1]": "podfile:b" },
|
|
96
|
+
opts: { multipart: true },
|
|
97
|
+
},
|
|
98
|
+
]);
|
|
99
|
+
expect(result).toMatchObject({ postId: "p1", attachments: 2 });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("refuses before any call: Instagram with no media, an account with no posts, no text", async () => {
|
|
103
|
+
await expect(run({ accountId: "acc_2", text: "Caption", provider: "instagram" })).rejects.toThrow(/Instagram needs at least one image or video/);
|
|
104
|
+
await expect(run({ accountId: "acc_3", text: "Hi", provider: "whatsapp" })).rejects.toThrow(/whatsapp account has no posts/);
|
|
105
|
+
await expect(run({ accountId: "acc_1", text: " ", provider: "linkedin" })).rejects.toThrow(/text is required/);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("as a LinkedIn company page: post_as beside the text, as JSON and in the multipart form", async () => {
|
|
109
|
+
const { calls } = await run({ accountId: "acc_1", text: "Hello", provider: "linkedin", postAs: "12345" });
|
|
110
|
+
expect(calls[0]).toMatchObject({ path: "/acc_1/posts", body: { text: "Hello", post_as: "12345" }, opts: undefined });
|
|
111
|
+
const multi = await run({ accountId: "acc_1", text: "Hi", provider: "linkedin", postAs: "12345", media: ["podfile:a"] });
|
|
112
|
+
expect(multi.calls[0]).toMatchObject({ body: { text: "Hi", post_as: "12345", "attachments[0]": "podfile:a" }, opts: { multipart: true } });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("a repost: quoted_post_id, with empty text a plain repost, with text a quote", async () => {
|
|
116
|
+
const plain = await run({ accountId: "acc_1", text: "", provider: "linkedin", quotedPostId: "WzEy" });
|
|
117
|
+
expect(plain.calls[0].body).toEqual({ text: "", quoted_post_id: "WzEy" });
|
|
118
|
+
const quote = await run({ accountId: "acc_1", text: "Worth a read", quotedPostId: "WzEy", postAs: "12345" });
|
|
119
|
+
expect(quote.calls[0].body).toEqual({ text: "Worth a read", post_as: "12345", quoted_post_id: "WzEy" });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("an empty company page or original is left out, and Instagram refuses both before any call", async () => {
|
|
123
|
+
const { calls } = await run({ accountId: "acc_1", text: "Hello", provider: "linkedin", postAs: " ", quotedPostId: "" });
|
|
124
|
+
expect(calls[0].body).toEqual({ text: "Hello" });
|
|
125
|
+
await expect(run({ accountId: "acc_2", text: "C", provider: "instagram", media: ["podfile:a"], quotedPostId: "WzEy" })).rejects.toThrow(/Instagram has no company page post or repost/);
|
|
126
|
+
await expect(run({ accountId: "acc_2", text: "C", provider: "instagram", media: ["podfile:a"], postAs: "1" })).rejects.toThrow(/Instagram has no company page post or repost/);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("linkedin_list_company_pages: a read of the same tool, listing each page's id and name", () => {
|
|
130
|
+
const pages = skill("linkedin_list_company_pages");
|
|
131
|
+
expect(pages).toMatchObject({ intent: "list_records", kind: "declarative", metadata: { readOnly: true }, parameters: { accountId: "string" } });
|
|
132
|
+
expect(pages.requires).toEqual(verb.requires);
|
|
133
|
+
expect(pages.providerSpec).toMatchObject({
|
|
134
|
+
tool: "unipile_linkedin",
|
|
135
|
+
method: "GET",
|
|
136
|
+
pathTemplate: "/{{accountId}}/linkedin/company/pages",
|
|
137
|
+
responseShape: { collectionPath: "data", item: { id: "id", name: "name" } },
|
|
138
|
+
});
|
|
139
|
+
// A read never maps into an app's intent: nothing publishes through it.
|
|
140
|
+
expect(pages.inputMap).toBeUndefined();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("a refused post throws, and a proposed call is never reported as published", async () => {
|
|
144
|
+
await expect(run({ accountId: "acc_1", text: "Hello" }, { status: 422, body: {} })).rejects.toThrow(/status 422/);
|
|
145
|
+
const { result } = await run({ accountId: "acc_1", text: "Hello" }, { proposed: true, proposalId: "pr1" });
|
|
146
|
+
expect(result).toEqual({ success: false, proposed: true, proposalId: "pr1" });
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("the account source is a read of the same tool that lists every account with the fields it names", () => {
|
|
150
|
+
const list = skill("unipile_list_accounts");
|
|
151
|
+
expect(list.metadata).toEqual({ readOnly: true });
|
|
152
|
+
expect(list.requires).toEqual(verb.requires);
|
|
153
|
+
expect(list.providerSpec).toMatchObject({ method: "GET", pathTemplate: "/accounts/" });
|
|
154
|
+
expect(list.providerSpec.query).toBeUndefined();
|
|
155
|
+
expect(Object.keys(list.providerSpec.responseShape.item)).toEqual(expect.arrayContaining(["id", "name", "provider"]));
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("one Unipile verb publishes for apps", () => {
|
|
159
|
+
const mapped = capability.spec.skills.filter((s) => s.inputMap && s.intent === "publish_post").map((s) => s.name);
|
|
160
|
+
expect(mapped).toEqual(["unipile_publish_post"]);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -22,6 +22,7 @@ import { describe, expect, it } from "vitest";
|
|
|
22
22
|
import { readFileSync, existsSync } from "node:fs";
|
|
23
23
|
import { dirname, join } from "node:path";
|
|
24
24
|
import { fileURLToPath } from "node:url";
|
|
25
|
+
import { stripComments } from "../../design-tokens/scripts/strip-comments.mjs";
|
|
25
26
|
|
|
26
27
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
27
28
|
const OWN_COPY = join(HERE, "validate-flow.ts");
|
|
@@ -40,11 +41,9 @@ const BACKEND_COPY = join(
|
|
|
40
41
|
* normalize whitespace.
|
|
41
42
|
*/
|
|
42
43
|
function normalizeFlowValidatorSource(src: string): string {
|
|
43
|
-
return src
|
|
44
|
+
return stripComments(src, "ts")
|
|
44
45
|
.replace(/import type \{[^}]*\} from ["']@synap\/database["'];/g, "")
|
|
45
46
|
.replace(/type AssertExact<[\s\S]*?void _outputTypesInSync;/g, "")
|
|
46
|
-
.replace(/\/\*[\s\S]*?\*\//g, "") // block comments (incl. JSDoc)
|
|
47
|
-
.replace(/\/\/.*$/gm, "") // line + trailing comments
|
|
48
47
|
.split("\n")
|
|
49
48
|
.map((l) => l.replace(/\s+/g, " ").trim())
|
|
50
49
|
.filter((l) => l.length > 0)
|
package/src/validate-flow.ts
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
* Until then: keep the constant arrays below identical to `@synap/database`
|
|
36
36
|
* `schema/automations.ts`.
|
|
37
37
|
*/
|
|
38
|
+
import { PLAYBOOK_RUN_MODES } from "@synap-core/types/automations";
|
|
38
39
|
|
|
39
40
|
// ── Contract constants (mirror schema/automations.ts — keep in sync) ──────────
|
|
40
41
|
|
|
@@ -65,9 +66,10 @@ export const FLOW_NODE_TYPES = [
|
|
|
65
66
|
"playbook_run",
|
|
66
67
|
] as const;
|
|
67
68
|
|
|
68
|
-
/** The
|
|
69
|
+
/** The 15 output types — `OutputNodeDef.data.outputType` (schema/automations.ts). */
|
|
69
70
|
export const FLOW_OUTPUT_TYPES = [
|
|
70
71
|
"notification",
|
|
72
|
+
"record_observation",
|
|
71
73
|
"entity_create",
|
|
72
74
|
"entity_update",
|
|
73
75
|
"facet_attach",
|
|
@@ -77,6 +79,9 @@ export const FLOW_OUTPUT_TYPES = [
|
|
|
77
79
|
"webhook",
|
|
78
80
|
"channel_message",
|
|
79
81
|
"session_update",
|
|
82
|
+
"complete_outcome",
|
|
83
|
+
"post_evidence",
|
|
84
|
+
"reevaluate_criterion",
|
|
80
85
|
"set_state",
|
|
81
86
|
] as const;
|
|
82
87
|
|
|
@@ -380,6 +385,21 @@ export function validateFlowDefinition(
|
|
|
380
385
|
}) that does not exist.`,
|
|
381
386
|
});
|
|
382
387
|
}
|
|
388
|
+
// An unknown `mode` would silently RUN (`readPlaybookRunMode` reads it
|
|
389
|
+
// as the default) — the opposite of what an author who typed
|
|
390
|
+
// "propose" wanted. Refused by name at the door instead.
|
|
391
|
+
if (
|
|
392
|
+
data.mode !== undefined &&
|
|
393
|
+
!(PLAYBOOK_RUN_MODES as readonly unknown[]).includes(data.mode)
|
|
394
|
+
) {
|
|
395
|
+
errors.push({
|
|
396
|
+
nodeId,
|
|
397
|
+
code: "playbook_run_bad_mode",
|
|
398
|
+
message: `Playbook-run node "${nodeId}" has mode ${JSON.stringify(
|
|
399
|
+
data.mode
|
|
400
|
+
)}; expected one of ${PLAYBOOK_RUN_MODES.join(", ")}.`,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
383
403
|
break;
|
|
384
404
|
}
|
|
385
405
|
|
|
@@ -671,7 +691,7 @@ function collectLoopBindingRefs(data: unknown): string[] {
|
|
|
671
691
|
// never evaluates (`_filterNote` and friends explain what DOWNSTREAM
|
|
672
692
|
// nodes do, and so legitimately quote `{{loop.item...}}` in prose).
|
|
673
693
|
// Reading them as live references made a correct flow fail validation:
|
|
674
|
-
//
|
|
694
|
+
// a (since-removed) grant capability was reported as "references {{loop.item}} but is
|
|
675
695
|
// not inside a loop body" purely because its note DESCRIBED the nodes
|
|
676
696
|
// that do. Kept identical in all three copies of this walker.
|
|
677
697
|
for (const [k, x] of Object.entries(v)) {
|
|
@@ -85,4 +85,63 @@ describe("validateTemplate onboarding contract", () => {
|
|
|
85
85
|
])
|
|
86
86
|
);
|
|
87
87
|
});
|
|
88
|
+
|
|
89
|
+
it("a steady-state brief needs no interview: purpose + anchors + fetch is valid", () => {
|
|
90
|
+
const result = validateTemplate(
|
|
91
|
+
templateWithOnboarding({
|
|
92
|
+
purpose: "Where records live.",
|
|
93
|
+
anchors: [{ profileSlug: "record", role: "root", limit: 1 }],
|
|
94
|
+
fetch: [{ profileSlug: "record", note: "the live ones" }],
|
|
95
|
+
}),
|
|
96
|
+
{}
|
|
97
|
+
);
|
|
98
|
+
expect(result, JSON.stringify(result.errors)).toEqual({ ok: true, errors: [] });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("an interview still requires its persona and collect targets", () => {
|
|
102
|
+
const result = validateTemplate(
|
|
103
|
+
templateWithOnboarding({ goal: "Set it up.", openingQuestions: ["Why?"] }),
|
|
104
|
+
{}
|
|
105
|
+
);
|
|
106
|
+
expect(result.errors.map((e) => e.rule)).toEqual(
|
|
107
|
+
expect.arrayContaining(["onboarding-framing-empty", "onboarding-collect-empty"])
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("rejects a brief that says nothing, bad anchors, bad fetch hints and bad minimums", () => {
|
|
112
|
+
const result = validateTemplate(
|
|
113
|
+
templateWithOnboarding({
|
|
114
|
+
anchors: [
|
|
115
|
+
{ profileSlug: "record", role: "root", seedRef: "Nope" },
|
|
116
|
+
{ profileSlug: "record", role: "root" },
|
|
117
|
+
{ profileSlug: "ghost", role: "context" },
|
|
118
|
+
],
|
|
119
|
+
fetch: [{ note: "no target" }],
|
|
120
|
+
collect: [{ profileSlug: "record", what: "x", min: 0 }],
|
|
121
|
+
}),
|
|
122
|
+
{}
|
|
123
|
+
);
|
|
124
|
+
expect(result.errors.map((e) => e.rule)).toEqual(
|
|
125
|
+
expect.arrayContaining([
|
|
126
|
+
"onboarding-brief-empty",
|
|
127
|
+
"onboarding-anchor-seedref-unknown",
|
|
128
|
+
"onboarding-anchor-invalid",
|
|
129
|
+
"onboarding-fetch-invalid",
|
|
130
|
+
"onboarding-collect-min-invalid",
|
|
131
|
+
])
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("template rules need a unique kebab key and an intent", () => {
|
|
136
|
+
const tpl = templateWithOnboarding(undefined);
|
|
137
|
+
tpl.rules = [
|
|
138
|
+
{ key: "use-approved", intent: "Use approved assets first." },
|
|
139
|
+
{ key: "use-approved", intent: "Dup." },
|
|
140
|
+
{ key: "Bad Key", intent: "" },
|
|
141
|
+
];
|
|
142
|
+
const rules = validateTemplate(tpl, {}).errors.map((e) => e.rule);
|
|
143
|
+
expect(rules).toEqual(
|
|
144
|
+
expect.arrayContaining(["rule-key-duplicate", "rule-key-invalid", "rule-intent-empty"])
|
|
145
|
+
);
|
|
146
|
+
});
|
|
88
147
|
});
|
package/src/validate.test.ts
CHANGED
|
@@ -15,7 +15,7 @@ import type { WorkspaceYaml } from "./types.js";
|
|
|
15
15
|
describe("validateTemplate — fires on a bad template", () => {
|
|
16
16
|
const bad = {
|
|
17
17
|
// meta.slug MISSING — required field.
|
|
18
|
-
meta: { name: "Bad", description: "d", icon: "x", color: "
|
|
18
|
+
meta: { name: "Bad", description: "d", icon: "x", color: "violet" },
|
|
19
19
|
workspace: { name: "Bad", description: "d" },
|
|
20
20
|
profiles: [{ slug: "gadget", displayName: "Gadget", properties: [] }],
|
|
21
21
|
automations: [
|
|
@@ -65,7 +65,7 @@ describe("validateTemplate — clean template", () => {
|
|
|
65
65
|
name: "Good",
|
|
66
66
|
description: "d",
|
|
67
67
|
icon: "x",
|
|
68
|
-
color: "
|
|
68
|
+
color: "violet",
|
|
69
69
|
},
|
|
70
70
|
workspace: { name: "Good", description: "d" },
|
|
71
71
|
profiles: [{ slug: "gadget", displayName: "Gadget", properties: [] }],
|
|
@@ -88,7 +88,7 @@ describe("validateTemplate — system Knowledge contract", () => {
|
|
|
88
88
|
name: "Knowledge consumer",
|
|
89
89
|
description: "d",
|
|
90
90
|
icon: "brain",
|
|
91
|
-
color: "
|
|
91
|
+
color: "violet",
|
|
92
92
|
},
|
|
93
93
|
workspace: { name: "Knowledge consumer", description: "d" },
|
|
94
94
|
profiles: [{ slug: "gadget", displayName: "Gadget", properties: [] }],
|
|
@@ -178,7 +178,7 @@ describe("validateTemplate — automation flow node contract", () => {
|
|
|
178
178
|
name: "Flowy",
|
|
179
179
|
description: "d",
|
|
180
180
|
icon: "x",
|
|
181
|
-
color: "
|
|
181
|
+
color: "violet",
|
|
182
182
|
},
|
|
183
183
|
workspace: { name: "Flowy", description: "d" },
|
|
184
184
|
profiles: [{ slug: "gadget", displayName: "Gadget", properties: [] }],
|
|
@@ -286,3 +286,70 @@ describe("validateTemplate — automation flow node contract", () => {
|
|
|
286
286
|
});
|
|
287
287
|
});
|
|
288
288
|
});
|
|
289
|
+
|
|
290
|
+
describe("validateTemplate — a pack needs no profile shell (D8)", () => {
|
|
291
|
+
const pack = (tags: string[], deps: unknown[]) =>
|
|
292
|
+
({
|
|
293
|
+
meta: {
|
|
294
|
+
slug: "acme-suite",
|
|
295
|
+
name: "Acme",
|
|
296
|
+
description: "d",
|
|
297
|
+
icon: "x",
|
|
298
|
+
color: "violet",
|
|
299
|
+
tags,
|
|
300
|
+
},
|
|
301
|
+
workspace: { name: "Acme", description: "d" },
|
|
302
|
+
profiles: [],
|
|
303
|
+
dependencies: deps,
|
|
304
|
+
}) as unknown as WorkspaceYaml;
|
|
305
|
+
const profileErrors = (t: WorkspaceYaml) =>
|
|
306
|
+
validateTemplate(t, {}).errors.filter((e) => e.rule === "profiles-required");
|
|
307
|
+
|
|
308
|
+
it("a suite that requires a workspace is valid without profiles", () => {
|
|
309
|
+
expect(
|
|
310
|
+
profileErrors(pack(["suite"], [{ slug: "crm", kind: "workspace", relation: "require" }]))
|
|
311
|
+
).toEqual([]);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("a suite requiring NO workspace still needs a profile", () => {
|
|
315
|
+
expect(profileErrors(pack(["suite"], []))).toHaveLength(1);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("a non-suite template without profiles is still refused", () => {
|
|
319
|
+
expect(
|
|
320
|
+
profileErrors(pack([], [{ slug: "crm", kind: "workspace", relation: "require" }]))
|
|
321
|
+
).toHaveLength(1);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe("validateTemplate — entityLinks[].required", () => {
|
|
326
|
+
const withLinks = (entityLinks: unknown[]) =>
|
|
327
|
+
({
|
|
328
|
+
meta: { slug: "links", name: "Links", description: "d", icon: "x", color: "violet" },
|
|
329
|
+
workspace: { name: "Links", description: "d" },
|
|
330
|
+
profiles: [
|
|
331
|
+
{ slug: "gadget", displayName: "Gadget", properties: [] },
|
|
332
|
+
{ slug: "widget", displayName: "Widget", properties: [] },
|
|
333
|
+
],
|
|
334
|
+
entityLinks,
|
|
335
|
+
}) as unknown as WorkspaceYaml;
|
|
336
|
+
const link = { sourceProfileSlug: "gadget", targetProfileSlug: "widget", type: "relates_to" };
|
|
337
|
+
const requiredErrors = (t: WorkspaceYaml) =>
|
|
338
|
+
validateTemplate(t, {}).errors.filter((e) => e.rule === "entitylink-required-not-boolean");
|
|
339
|
+
|
|
340
|
+
it("accepts required: true, required: false and no required", () => {
|
|
341
|
+
const result = validateTemplate(
|
|
342
|
+
withLinks([{ ...link, required: true }, { ...link, type: "part_of", required: false }, { ...link, type: "mentions" }]),
|
|
343
|
+
{}
|
|
344
|
+
);
|
|
345
|
+
expect(result, JSON.stringify(result.errors)).toEqual({ ok: true, errors: [] });
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("refuses a required that is not a boolean, naming the link", () => {
|
|
349
|
+
for (const bad of ["yes", 1, "true", null]) {
|
|
350
|
+
const errors = requiredErrors(withLinks([{ ...link, required: bad }]));
|
|
351
|
+
expect(errors, String(bad)).toHaveLength(1);
|
|
352
|
+
expect(errors[0]!.message).toContain("gadget→widget (relates_to)");
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
});
|