@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,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The static asset edit loop, W1 (founder D0-D2, 2 Nov 2026; build plan pod document ab3e0a5a):
|
|
3
|
+
*
|
|
4
|
+
* - ONE edit-run contract for every engine, written once in the `edit-loop` skill: the `layer-<pass>` slots,
|
|
5
|
+
* `studio`, `studio-app`, `needs-tool-<tool>`, the three answers, and the rules that keep the person's edits
|
|
6
|
+
* safe. The Draft goal and `facecam-edit` name it instead of spelling it again. The keys keep their names:
|
|
7
|
+
* Content Studio, Relay and runs in flight read them.
|
|
8
|
+
* - Carousels, LinkedIn PDFs and image posts are layered compositions in the pod (`carousel-format`); decks,
|
|
9
|
+
* PDF documents and presentations are built in Open Design (`open-design-edit`). The VISUAL branch no longer
|
|
10
|
+
* routes to the whiteboard generators.
|
|
11
|
+
* - The kinds gain their engines and formats additively (the reconciler is add-only).
|
|
12
|
+
*
|
|
13
|
+
* Read from the YAML source and the skill JSON, so an edit is seen without a rebuild.
|
|
14
|
+
*/
|
|
15
|
+
import { readFileSync } from "fs";
|
|
16
|
+
import { dirname, join } from "path";
|
|
17
|
+
import { fileURLToPath } from "url";
|
|
18
|
+
import { load } from "js-yaml";
|
|
19
|
+
import { describe, expect, it } from "vitest";
|
|
20
|
+
import type { TemplatePlaybook, WorkspaceYaml } from "./types.js";
|
|
21
|
+
|
|
22
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const yaml = (file: string) => load(readFileSync(join(here, file), "utf8")) as WorkspaceYaml;
|
|
24
|
+
const skill = (slug: string) =>
|
|
25
|
+
JSON.parse(readFileSync(join(here, `../skills/${slug}/${slug}.skill.json`), "utf8")) as {
|
|
26
|
+
meta: { slug: string; description: string };
|
|
27
|
+
spec: { slug: string; body: string; description: string };
|
|
28
|
+
};
|
|
29
|
+
const flat = (s: string) => s.replace(/\s+/g, " ");
|
|
30
|
+
|
|
31
|
+
const source = yaml("content-os.yaml");
|
|
32
|
+
const produce = (source.playbooks ?? []).find((p) => p.name === "Produce Content") as TemplatePlaybook | undefined;
|
|
33
|
+
const draftGoal = flat(produce?.stages?.find((s) => s.key === "draft")?.goal ?? "");
|
|
34
|
+
const loop = skill("edit-loop").spec.body;
|
|
35
|
+
|
|
36
|
+
describe("the edit-loop skill holds the one edit-run contract", () => {
|
|
37
|
+
it("is a package whose slug is its folder", () => {
|
|
38
|
+
for (const slug of ["edit-loop", "open-design-edit"]) {
|
|
39
|
+
expect(skill(slug).meta.slug).toBe(slug);
|
|
40
|
+
expect(skill(slug).spec.slug).toBe(slug);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("keeps every key Content Studio reads, unrenamed", () => {
|
|
45
|
+
for (const key of ['"layer-<pass>"', '"studio"', '"studio-app"', '"needs-tool-<tool>"', "retireOutput studio"]) {
|
|
46
|
+
expect(loop).toContain(key);
|
|
47
|
+
}
|
|
48
|
+
// The video pass keys, in build order (Content Studio's PASSES).
|
|
49
|
+
const video = ["`cut`", "`background`", "`colour`", "`captions`", "`graphics`"].map((k) => loop.indexOf(k));
|
|
50
|
+
expect(video.every((i) => i > 0)).toBe(true);
|
|
51
|
+
expect([...video].sort((a, b) => a - b)).toEqual(video);
|
|
52
|
+
// The static passes: structure and copy, visual, polish.
|
|
53
|
+
expect(loop).toContain("`structure` (structure and copy), `visual`, `polish`");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("asks the same three answers on every pass, and says what each does", () => {
|
|
57
|
+
expect(loop).toContain(
|
|
58
|
+
"options **Approve** (recommended: keeps this pass and builds the next), **Rework** (redoes this pass only) and **Use my edit** (the person changed this pass in its editor)",
|
|
59
|
+
);
|
|
60
|
+
for (const answer of ["- **Approve**:", "- **Rework**:", "- **Use my edit**:", "- **Remove this pass**"]) {
|
|
61
|
+
expect(loop).toContain(answer);
|
|
62
|
+
}
|
|
63
|
+
expect(loop).toContain("keep it as the pass and change nothing of it");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("states the static rules: version first, diff per slide, never regenerate, no write on the person's turn, acknowledge", () => {
|
|
67
|
+
for (const rule of [
|
|
68
|
+
"**A version before every pass of yours.**",
|
|
69
|
+
"**Work from the diff, never from the prompt.**",
|
|
70
|
+
"slide by slide (by slide number, then layer id)",
|
|
71
|
+
"Never regenerate a pass, a slide or a page from the brief or the script",
|
|
72
|
+
"**Never write while a pass is the person's.**",
|
|
73
|
+
"**Acknowledge every answer in the room.**",
|
|
74
|
+
"`Got <answer> on <pass>: <what you do now>`",
|
|
75
|
+
]) {
|
|
76
|
+
expect(loop).toContain(rule);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("routes each format to its engine skill", () => {
|
|
81
|
+
expect(loop).toContain("| Carousel, LinkedIn PDF | Layered composition in the pod | `carousel-format`");
|
|
82
|
+
expect(loop).toContain("| Deck, PDF document, presentation | Open Design on the person's Mac | `open-design-edit`");
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("the Draft goal and facecam-edit name the contract instead of spelling it twice", () => {
|
|
87
|
+
it("the Draft goal loads edit-loop and routes the static formats to their engines", () => {
|
|
88
|
+
expect(draftGoal).toContain("built pass by pass through the edit-loop skill and its engine's skill");
|
|
89
|
+
expect(draftGoal).toContain("a layered composition saved on the post (carousel-format; passes layer-structure, layer-visual, layer-polish");
|
|
90
|
+
expect(draftGoal).toContain("Open Design (open-design-edit, same passes)");
|
|
91
|
+
for (const rule of [
|
|
92
|
+
"Before each of your passes record a version",
|
|
93
|
+
"never regenerate from the prompt",
|
|
94
|
+
"never write while a pass is the person's",
|
|
95
|
+
"acknowledge each answer in the room",
|
|
96
|
+
]) {
|
|
97
|
+
expect(draftGoal).toContain(rule);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("every Produce Content stage goal stays under the pod's 5,000-character cap, the Draft goal shorter than before", () => {
|
|
102
|
+
for (const s of produce?.stages ?? []) expect((s.goal ?? "").length).toBeLessThanOrEqual(5000);
|
|
103
|
+
// 4,911 on 63f6f004a: moving the contract into the skill must not grow it.
|
|
104
|
+
expect((produce?.stages?.find((s) => s.key === "draft")?.goal ?? "").length).toBeLessThanOrEqual(4911);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("facecam-edit points at edit-loop, and keeps no second copy of the needs-tool slot", () => {
|
|
108
|
+
const body = skill("facecam-edit").spec.body;
|
|
109
|
+
expect(body).toContain("**The passes follow the `edit-loop` skill**");
|
|
110
|
+
expect(body).toContain("declare ONE `needs-tool-<tool>` slot per missing tool, shaped and answered as the `edit-loop` skill spells out");
|
|
111
|
+
// The slot as facecam-edit spelled it before 2 Nov: now only in edit-loop.
|
|
112
|
+
expect(body).not.toContain('{label:"Use another agent"');
|
|
113
|
+
expect(body).not.toContain('key:"needs-tool-<tool>"');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("the VISUAL branch uses the edit loop", () => {
|
|
118
|
+
it("drops the whiteboard generators from the process and the creative director", () => {
|
|
119
|
+
const process = [produce?.goalTemplate ?? "", ...(produce?.stages ?? []).map((s) => s.goal ?? "")].join(" ");
|
|
120
|
+
for (const gone of ["generate_carousel", "generate_deck", "whiteboard"]) {
|
|
121
|
+
expect(process).not.toContain(gone);
|
|
122
|
+
expect(skill("creative-director").spec.body).not.toContain(gone === "whiteboard" ? "onto a whiteboard board" : gone);
|
|
123
|
+
}
|
|
124
|
+
expect(flat(produce?.goalTemplate ?? "")).toContain("VISUAL: build it pass by pass through the edit-loop skill");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("the creative director routes carousel/image to the layered composition and decks to Open Design", () => {
|
|
128
|
+
const cd = skill("creative-director").spec.body;
|
|
129
|
+
expect(cd).toContain("| carousel / image | VISUAL — approved carousel template → a layered composition on the post |");
|
|
130
|
+
expect(cd).toContain("| deck / document | VISUAL — approved deck template → Open Design on the person's Mac |");
|
|
131
|
+
expect(cd).toContain("`carousel-format` (carousel, LinkedIn PDF, image), `open-design-edit` (deck, PDF document). Then build pass by pass with `edit-loop`.");
|
|
132
|
+
// The frames and the script gate stay.
|
|
133
|
+
expect(cd).toContain("| hot-take | Claim · Tension · Reason · Invitation |");
|
|
134
|
+
expect(cd).toContain("### 4. Script gate — the written version comes first");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("creative-director stays always-on: its body fits the 2,000-token budget (length / 4), or the IS demotes it", () => {
|
|
138
|
+
// @synap-core/types space-brief `estimateSkillTokens`; the validator only warns (always-skill-over-budget).
|
|
139
|
+
expect(Math.ceil(skill("creative-director").spec.body.length / 4)).toBeLessThanOrEqual(2000);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("video-toolbox (name pinned) maps the static jobs to their engines", () => {
|
|
143
|
+
const toolbox = skill("video-toolbox");
|
|
144
|
+
expect(toolbox.meta.slug).toBe("video-toolbox");
|
|
145
|
+
expect(toolbox.spec.body).toContain("| Carousel, LinkedIn PDF, image post (not a video) | a **layered composition**");
|
|
146
|
+
expect(toolbox.spec.body).toContain("| Deck, PDF document, presentation (not a video) | **Open Design**");
|
|
147
|
+
expect(toolbox.spec.body).toContain("`open-design-edit`, then `edit-loop`");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("carousel-format builds the layered composition through the revision-checked composition door", () => {
|
|
151
|
+
const body = skill("carousel-format").spec.body;
|
|
152
|
+
for (const part of [
|
|
153
|
+
"### Build it: the layered composition",
|
|
154
|
+
'"type": "rect" | "image" | "asset" | "text" | "chip"',
|
|
155
|
+
"`tells`",
|
|
156
|
+
"a headline 96 px or more (104 by default), body 36 px or more (40 by default)",
|
|
157
|
+
"`baseRevision` = that revision",
|
|
158
|
+
"Carousel NN vX — slide n/N",
|
|
159
|
+
"the composition `made_for` the post",
|
|
160
|
+
"Agents never publish",
|
|
161
|
+
]) {
|
|
162
|
+
expect(body).toContain(part);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("open-design-edit follows the W0 recipe: privacy, project locations (never import/folder), ai versions, export", () => {
|
|
167
|
+
const body = skill("open-design-edit").spec.body;
|
|
168
|
+
for (const part of [
|
|
169
|
+
'`telemetry` must be `{ "metrics": false, "content": false }`',
|
|
170
|
+
"`enabled` must be `false`",
|
|
171
|
+
"`GET http://127.0.0.1:<port>/api/health`",
|
|
172
|
+
"`PUT /api/project-locations`",
|
|
173
|
+
"`POST /api/project-locations/scan`",
|
|
174
|
+
"Never `POST /api/import/folder`",
|
|
175
|
+
'`<section class="slide">`',
|
|
176
|
+
"**Never write into `out/` or `dist/`**",
|
|
177
|
+
'`{ "source": "ai", "label": "<pass> v<n>", "prompt":',
|
|
178
|
+
"`manual` versions",
|
|
179
|
+
"`POST /api/projects/<id>/export/pdf-image`",
|
|
180
|
+
"`needs-tool-open-design`",
|
|
181
|
+
"od-design-system-project/v1",
|
|
182
|
+
]) {
|
|
183
|
+
expect(body).toContain(part);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("Content OS installs and links the new skills, and the kinds grow additively", () => {
|
|
189
|
+
it("requires and links edit-loop and open-design-edit", () => {
|
|
190
|
+
for (const slug of ["edit-loop", "open-design-edit"]) {
|
|
191
|
+
expect((source.dependencies ?? []).some((d) => d.slug === slug && d.kind === "skill" && (d.relation ?? "require") === "require")).toBe(true);
|
|
192
|
+
expect((source.skills ?? []).some((s) => s.slug === slug)).toBe(true);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("adds the static engines and formats without dropping a value, in Content OS", () => {
|
|
197
|
+
for (const file of ["content-os.yaml"]) {
|
|
198
|
+
const profiles = yaml(file).profiles;
|
|
199
|
+
const prop = (profile: string, slug: string) =>
|
|
200
|
+
profiles.find((p) => p.slug === profile)?.properties.find((d) => d.slug === slug) as { enumValues?: string[] } | undefined;
|
|
201
|
+
expect(prop("content-composition", "render-engine")?.enumValues).toEqual(["hyperframes", "layered", "open-design"]);
|
|
202
|
+
expect(prop("render-job", "output-format")?.enumValues).toEqual(["mp4", "gif", "webm", "mp3", "pdf", "png", "pptx"]);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("adds no stage and no required param", () => {
|
|
207
|
+
expect(produce?.stages?.map((s) => s.key)).toEqual(["brief", "script", "draft", "review", "schedule", "publish"]);
|
|
208
|
+
expect((produce?.params ?? []).filter((p) => p.required).map((p) => p.name)).toEqual(["post", "platform"]);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Publish due posts" (decision A, founder, 3 Nov: a Scheduled post goes out
|
|
3
|
+
* through Unipile on its date, with no second approval) and the fields it
|
|
4
|
+
* reads, pinned on the template that owns them (content-os owns `post` and
|
|
5
|
+
* `platform`).
|
|
6
|
+
*
|
|
7
|
+
* What it proves, each from the authored flow, never a hand copy of it:
|
|
8
|
+
* - the flow passes the catalog's own flow validator;
|
|
9
|
+
* - every property the flow READS from the due post and every property it
|
|
10
|
+
* WRITES is declared on `post`, and every value it writes to a select or the
|
|
11
|
+
* lifecycle is one of its options (an undeclared key would be stored
|
|
12
|
+
* unmodeled and an unknown option refused: the send would go out and the
|
|
13
|
+
* post would never say so);
|
|
14
|
+
* - the send's input maps the verb's required parameters, and only declared
|
|
15
|
+
* ones (`unipile_publish_post` in the unipile-linkedin capability);
|
|
16
|
+
* - the send is never retried, its failure continues to a branch that writes
|
|
17
|
+
* publish-outcome failed and publish-error, and the post is claimed
|
|
18
|
+
* (and the claim checked as written) before it is sent;
|
|
19
|
+
* - one post per run (a loop would abort a failed item with no failure
|
|
20
|
+
* branch): the send is never inside a loop, and it ships paused;
|
|
21
|
+
* - accounts and destinations (founder, 4 Nov, L9): the send passes the
|
|
22
|
+
* company page and the repost's original; a sent post keeps its Unipile
|
|
23
|
+
* id and queues the reposts waiting for it, with that id, in a loop whose
|
|
24
|
+
* body is that one write.
|
|
25
|
+
*
|
|
26
|
+
* NOT covered: the engine running it (query, transform, condition and template
|
|
27
|
+
* semantics live in synap-backend's automation executor; read there on
|
|
28
|
+
* 3 Nov: `to_ms`, numeric `$lte` on a property, `continueOnError` storing
|
|
29
|
+
* `{ error }`, entity_update returning `status: "updated"`); Unipile accepting
|
|
30
|
+
* the post.
|
|
31
|
+
*/
|
|
32
|
+
import { readFileSync } from "node:fs";
|
|
33
|
+
import { join } from "node:path";
|
|
34
|
+
import { describe, expect, it } from "vitest";
|
|
35
|
+
import { WORKSPACE_TEMPLATES } from "./templates.js";
|
|
36
|
+
import type { TemplateProfile, WorkspaceYaml } from "./types.js";
|
|
37
|
+
import { validateFlowDefinition } from "./validate-flow.js";
|
|
38
|
+
|
|
39
|
+
const contentOs = WORKSPACE_TEMPLATES["content-os"] as WorkspaceYaml;
|
|
40
|
+
const profile = (slug: string): TemplateProfile => {
|
|
41
|
+
const p = (contentOs.profiles ?? []).find((x) => x.slug === slug);
|
|
42
|
+
if (!p) throw new Error(`content-os declares no ${slug}`);
|
|
43
|
+
return p;
|
|
44
|
+
};
|
|
45
|
+
const post = profile("post");
|
|
46
|
+
const declared = new Set((post.properties ?? []).map((p) => p.slug));
|
|
47
|
+
const optionsOf = (slug: string): string[] => {
|
|
48
|
+
const prop = (post.properties ?? []).find((p) => p.slug === slug) as { enumValues?: unknown } | undefined;
|
|
49
|
+
return Array.isArray(prop?.enumValues) ? (prop.enumValues as string[]) : [];
|
|
50
|
+
};
|
|
51
|
+
const lifecycle = (post as { lifecycle?: { property: string; options: Array<{ key: string }> } }).lifecycle;
|
|
52
|
+
|
|
53
|
+
const automation = (contentOs.automations ?? []).find((a) => a.key === "content-os-publish-due-posts");
|
|
54
|
+
type Node = { id: string; type: string; data: Record<string, any> };
|
|
55
|
+
type Edge = { id: string; source: string; target: string; sourceHandle?: string };
|
|
56
|
+
const nodes = (automation?.flow.nodes ?? []) as Node[];
|
|
57
|
+
const edges = (automation?.flow.edges ?? []) as Edge[];
|
|
58
|
+
const node = (id: string) => {
|
|
59
|
+
const n = nodes.find((x) => x.id === id);
|
|
60
|
+
if (!n) throw new Error(`no node ${id}`);
|
|
61
|
+
return n;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const unipile = JSON.parse(
|
|
65
|
+
readFileSync(join(import.meta.dirname, "..", "capabilities", "unipile-linkedin.capability.json"), "utf8"),
|
|
66
|
+
) as { spec: { skills: Array<{ name: string; parameters?: Record<string, string> }> } };
|
|
67
|
+
const verb = unipile.spec.skills.find((s) => s.name === "unipile_publish_post");
|
|
68
|
+
|
|
69
|
+
/** Every `{{steps.due.output.entities.0.properties.<key>}}` the flow reads, from its whole JSON. */
|
|
70
|
+
const READ = /\{\{steps\.due\.output\.entities\.0\.properties\.([a-z0-9-]+)\}\}/g;
|
|
71
|
+
|
|
72
|
+
describe("Publish due posts — the automation", () => {
|
|
73
|
+
it("ships paused, every 15 minutes, and passes the catalog's flow validator", () => {
|
|
74
|
+
expect(automation).toMatchObject({ name: "Publish due posts", triggerType: "cron", status: "paused", triggerConfig: { expression: "*/15 * * * *" } });
|
|
75
|
+
const result = validateFlowDefinition(automation?.flow, { verbExists: (v) => v === "unipile_publish_post" });
|
|
76
|
+
expect(result.errors).toEqual([]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("sends one post per run, never inside a loop", () => {
|
|
80
|
+
expect(node("due").data.limit).toBe(1);
|
|
81
|
+
// The only loop queues reposts after the send: nothing that sends, and
|
|
82
|
+
// nothing before the send, is reachable from it.
|
|
83
|
+
const loops = nodes.filter((n) => n.type === "loop");
|
|
84
|
+
expect(loops.map((n) => n.id)).toEqual(["each-repost"]);
|
|
85
|
+
const reach = new Set<string>();
|
|
86
|
+
const stack = ["each-repost"];
|
|
87
|
+
while (stack.length) {
|
|
88
|
+
const id = stack.pop() as string;
|
|
89
|
+
for (const e of edges.filter((x) => x.source === id)) if (!reach.has(e.target)) reach.add(e.target), stack.push(e.target);
|
|
90
|
+
}
|
|
91
|
+
expect([...reach]).toEqual(["arm-repost"]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("finds only an armed Scheduled post whose time has come, oldest first", () => {
|
|
95
|
+
// The filter is a templated JSON string: with a number for its one placeholder it must parse.
|
|
96
|
+
const filter = JSON.parse(String(node("due").data.filter).replace("{{steps.now.output.result}}", "1762416000000"));
|
|
97
|
+
expect(filter).toEqual({ "post-status": "Scheduled", "publish-outcome": "queued", "publish-at-ms": { $lte: 1762416000000 } });
|
|
98
|
+
expect(lifecycle?.property).toBe("post-status");
|
|
99
|
+
expect(lifecycle?.options.map((o) => o.key)).toContain("Scheduled");
|
|
100
|
+
for (const key of Object.keys(filter)) expect(declared, key).toContain(key);
|
|
101
|
+
expect(optionsOf("publish-outcome")).toContain("queued");
|
|
102
|
+
expect(node("due").data).toMatchObject({ profileSlug: "post", orderBy: "properties.publish-at-ms", orderDir: "asc" });
|
|
103
|
+
expect(node("now").data.expression).toBe("{{trigger.payload.scheduledAt}} | to_ms");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("reads only declared post properties", () => {
|
|
107
|
+
const reads = [...JSON.stringify(automation?.flow).matchAll(READ)].map((m) => m[1]);
|
|
108
|
+
// Non-vacuity: it reads the frozen fields, the company page and the repost's original among them.
|
|
109
|
+
expect(new Set(reads)).toEqual(
|
|
110
|
+
new Set(["publish-account-id", "publish-text", "publish-media", "publish-as-organization", "publish-quote-id"]),
|
|
111
|
+
);
|
|
112
|
+
for (const key of reads) expect(declared, key).toContain(key);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("sends through unipile_publish_post with its required parameters, and only declared ones", () => {
|
|
116
|
+
const send = node("send");
|
|
117
|
+
expect(send.type).toBe("capability");
|
|
118
|
+
expect(send.data.verbId).toBe("unipile_publish_post");
|
|
119
|
+
const params = verb?.parameters ?? {};
|
|
120
|
+
const required = Object.entries(params).filter(([, t]) => !t.endsWith("?")).map(([k]) => k);
|
|
121
|
+
expect(required.sort()).toEqual(["accountId", "text"]);
|
|
122
|
+
for (const k of required) expect(Object.keys(send.data.inputMapping)).toContain(k);
|
|
123
|
+
for (const k of Object.keys(send.data.inputMapping)) expect(Object.keys(params)).toContain(k);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("claims the post before it sends, and sends only once the claim is written", () => {
|
|
127
|
+
expect(node("claim").data).toMatchObject({ outputType: "entity_update", config: { properties: { "publish-outcome": "sending" } } });
|
|
128
|
+
expect(optionsOf("publish-outcome")).toContain("sending");
|
|
129
|
+
expect(node("claimed").data.expression).toBe("steps.claim.output.status === 'updated'");
|
|
130
|
+
expect(edges).toEqual(
|
|
131
|
+
expect.arrayContaining([
|
|
132
|
+
expect.objectContaining({ source: "any-due", target: "claim", sourceHandle: "yes" }),
|
|
133
|
+
expect.objectContaining({ source: "claim", target: "claimed" }),
|
|
134
|
+
expect.objectContaining({ source: "claimed", target: "send", sourceHandle: "yes" }),
|
|
135
|
+
]),
|
|
136
|
+
);
|
|
137
|
+
// Nothing else leads to the send.
|
|
138
|
+
expect(edges.filter((e) => e.target === "send").map((e) => e.source)).toEqual(["claimed"]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("is never retried, and a failure goes to the branch that writes it on the post", () => {
|
|
142
|
+
expect(node("send").data.errorHandling).toEqual({ continueOnError: true });
|
|
143
|
+
expect(edges).toEqual(
|
|
144
|
+
expect.arrayContaining([
|
|
145
|
+
expect.objectContaining({ source: "send", target: "sent" }),
|
|
146
|
+
expect.objectContaining({ source: "sent", target: "published", sourceHandle: "yes" }),
|
|
147
|
+
expect.objectContaining({ source: "sent", target: "failed", sourceHandle: "no" }),
|
|
148
|
+
]),
|
|
149
|
+
);
|
|
150
|
+
expect(node("sent").data.expression).toBe("steps.send.output.error === ''");
|
|
151
|
+
expect(node("failed").data.config.properties).toEqual({ "publish-outcome": "failed", "publish-error": "{{steps.send.output.error}}" });
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("writes only declared properties, and only options a select or the lifecycle has", () => {
|
|
155
|
+
const writes = nodes.filter((n) => n.type === "output" && n.data.outputType === "entity_update");
|
|
156
|
+
expect(writes.map((w) => w.id).sort()).toEqual(["arm-repost", "claim", "failed", "link", "published", "remember-id"]);
|
|
157
|
+
for (const w of writes) {
|
|
158
|
+
// Every write is on the due post, except the one that queues each of its reposts.
|
|
159
|
+
expect(w.data.config.entityId).toBe(w.id === "arm-repost" ? "{{loop.item.id}}" : "{{steps.due.output.entities.0.id}}");
|
|
160
|
+
for (const [key, value] of Object.entries(w.data.config.properties as Record<string, string>)) {
|
|
161
|
+
expect(declared, `${w.id} writes ${key}`).toContain(key);
|
|
162
|
+
if (key === "post-status") expect(lifecycle?.options.map((o) => o.key)).toContain(value);
|
|
163
|
+
if (key === "publish-outcome") expect(optionsOf(key)).toContain(value);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
expect(node("published").data.config.properties).toMatchObject({ "post-status": "Published", "publish-outcome": "succeeded" });
|
|
167
|
+
// The link is its own write and may fail alone: Unipile may return none.
|
|
168
|
+
expect(node("link").data).toMatchObject({ config: { properties: { "post-url": "{{steps.send.output.shareUrl}}" } }, errorHandling: { continueOnError: true } });
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("Publish due posts — the fields Content Studio freezes when a post is approved", () => {
|
|
173
|
+
it("declares the frozen fields on post, and the Unipile account on platform", () => {
|
|
174
|
+
for (const key of ["publish-text", "publish-account-id", "publish-media", "publish-at-ms"]) expect(declared, key).toContain(key);
|
|
175
|
+
expect((post.properties ?? []).find((p) => p.slug === "publish-at-ms")?.valueType).toBe("number");
|
|
176
|
+
expect((post.properties ?? []).find((p) => p.slug === "publish-media")?.valueType).toBe("array");
|
|
177
|
+
expect(optionsOf("publish-outcome")).toEqual(["succeeded", "failed", "queued", "sending", "waiting"]);
|
|
178
|
+
expect((profile("platform").properties ?? []).map((p) => p.slug)).toContain("unipile-account-id");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("Produce Content's Schedule arms the post for Unipile, and its Publish says who writes Published", () => {
|
|
182
|
+
const pb = (contentOs.playbooks ?? []).find((p) => p.name === "Produce Content") as
|
|
183
|
+
| { stages?: Array<{ key: string; goal?: string }> }
|
|
184
|
+
| undefined;
|
|
185
|
+
const schedule = pb?.stages?.find((s) => s.key === "schedule")?.goal ?? "";
|
|
186
|
+
for (const word of [
|
|
187
|
+
"unipile-account-id",
|
|
188
|
+
"publish-account-id",
|
|
189
|
+
"publish-text",
|
|
190
|
+
"publish-media",
|
|
191
|
+
"publish-at-ms",
|
|
192
|
+
"publish-outcome queued",
|
|
193
|
+
"Publish due posts",
|
|
194
|
+
"account-owner",
|
|
195
|
+
"publish-as-organization",
|
|
196
|
+
"post-copy-to",
|
|
197
|
+
"post-reshare-on",
|
|
198
|
+
"post-copy-of",
|
|
199
|
+
"publish-outcome waiting",
|
|
200
|
+
])
|
|
201
|
+
expect(schedule, word).toContain(word);
|
|
202
|
+
const publish = pb?.stages?.find((s) => s.key === "publish")?.goal ?? "";
|
|
203
|
+
expect(publish).toContain("Publish due posts");
|
|
204
|
+
expect(publish).toContain("publish-outcome failed");
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe("Publish due posts — accounts and destinations (founder, 4 Nov)", () => {
|
|
209
|
+
const platform = profile("platform");
|
|
210
|
+
const platformProp = (slug: string) => (platform.properties ?? []).find((p) => p.slug === slug) as { enumValues?: unknown; valueType?: string } | undefined;
|
|
211
|
+
const postProp = (slug: string) => (post.properties ?? []).find((p) => p.slug === slug) as
|
|
212
|
+
| { enumValues?: unknown; valueType?: string; targetProfileSlug?: string }
|
|
213
|
+
| undefined;
|
|
214
|
+
|
|
215
|
+
it("a platform row is one account: Personal or Company, and a LinkedIn page id", () => {
|
|
216
|
+
expect(platformProp("account-owner")?.enumValues).toEqual(["Personal", "Company"]);
|
|
217
|
+
expect(platformProp("linkedin-organization-id")?.valueType).toBe("string");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("a post names its other accounts, and a post sent for another says which and how", () => {
|
|
221
|
+
expect(postProp("post-copy-to")?.valueType).toBe("array");
|
|
222
|
+
expect(postProp("post-reshare-on")?.valueType).toBe("array");
|
|
223
|
+
expect(postProp("post-copy-of")).toMatchObject({ valueType: "entity_id", targetProfileSlug: "post" });
|
|
224
|
+
expect(postProp("post-send-mode")?.enumValues).toEqual(["copy", "reshare"]);
|
|
225
|
+
for (const key of ["publish-as-organization", "publish-quote-id", "publish-post-id"]) expect(postProp(key)?.valueType, key).toBe("string");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("the send passes the company page and the repost's original, and the verb declares both", () => {
|
|
229
|
+
expect(node("send").data.inputMapping).toMatchObject({
|
|
230
|
+
postAs: "{{steps.due.output.entities.0.properties.publish-as-organization}}",
|
|
231
|
+
quotedPostId: "{{steps.due.output.entities.0.properties.publish-quote-id}}",
|
|
232
|
+
});
|
|
233
|
+
expect(verb?.parameters).toMatchObject({ postAs: "string?", quotedPostId: "string?" });
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("a sent post keeps its Unipile id, then queues its waiting reposts with that id", () => {
|
|
237
|
+
expect(edges).toEqual(
|
|
238
|
+
expect.arrayContaining([
|
|
239
|
+
expect.objectContaining({ source: "published", target: "link" }),
|
|
240
|
+
expect.objectContaining({ source: "link", target: "has-id" }),
|
|
241
|
+
expect.objectContaining({ source: "has-id", target: "remember-id", sourceHandle: "yes" }),
|
|
242
|
+
expect.objectContaining({ source: "remember-id", target: "reposts" }),
|
|
243
|
+
expect.objectContaining({ source: "reposts", target: "each-repost" }),
|
|
244
|
+
expect.objectContaining({ source: "each-repost", target: "arm-repost" }),
|
|
245
|
+
]),
|
|
246
|
+
);
|
|
247
|
+
// Only a sent post gets there: the failure branch leads nowhere near it.
|
|
248
|
+
expect(edges.filter((e) => e.target === "has-id").map((e) => e.source)).toEqual(["link"]);
|
|
249
|
+
expect(node("has-id").data.expression).toBe("steps.send.output.postId !== ''");
|
|
250
|
+
expect(node("remember-id").data).toMatchObject({
|
|
251
|
+
config: { properties: { "publish-post-id": "{{steps.send.output.postId}}" } },
|
|
252
|
+
errorHandling: { continueOnError: true },
|
|
253
|
+
});
|
|
254
|
+
const filter = JSON.parse(String(node("reposts").data.filter).replace("{{steps.due.output.entities.0.id}}", "p1"));
|
|
255
|
+
expect(filter).toEqual({ "post-copy-of": "p1", "publish-outcome": "waiting" });
|
|
256
|
+
expect(node("reposts").data.profileSlug).toBe("post");
|
|
257
|
+
expect(optionsOf("publish-outcome")).toContain("waiting");
|
|
258
|
+
expect(node("each-repost").data).toMatchObject({ iteratorExpression: "steps.reposts.output.entities", itemVariable: "repost" });
|
|
259
|
+
expect(node("arm-repost").data.config.properties).toEqual({ "publish-quote-id": "{{steps.send.output.postId}}", "publish-outcome": "queued" });
|
|
260
|
+
});
|
|
261
|
+
});
|