@sweny-ai/core 0.1.77 → 0.1.79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/dist/browser.d.ts +11 -5
  2. package/dist/browser.js +11 -11
  3. package/dist/claude.d.ts +16 -0
  4. package/dist/claude.js +98 -9
  5. package/dist/cli/config.d.ts +22 -0
  6. package/dist/cli/config.js +56 -9
  7. package/dist/cli/main.d.ts +1 -1
  8. package/dist/cli/main.js +56 -57
  9. package/dist/cli/marketplace.js +35 -9
  10. package/dist/cli/publish.d.ts +10 -0
  11. package/dist/cli/publish.js +22 -14
  12. package/dist/loader.d.ts +40 -0
  13. package/dist/loader.js +73 -0
  14. package/dist/mcp-catalog.d.ts +47 -0
  15. package/dist/mcp-catalog.js +304 -0
  16. package/dist/mcp.d.ts +6 -9
  17. package/dist/mcp.js +24 -314
  18. package/dist/schema.d.ts +183 -151
  19. package/dist/schema.js +49 -7
  20. package/dist/skills/custom-loader.d.ts +40 -0
  21. package/dist/skills/custom-loader.js +107 -32
  22. package/dist/skills/index.d.ts +13 -1
  23. package/dist/skills/index.js +12 -3
  24. package/dist/studio.js +4 -1
  25. package/dist/templates.d.ts +29 -8
  26. package/dist/templates.js +30 -20
  27. package/dist/types.d.ts +12 -0
  28. package/dist/verify.js +96 -7
  29. package/package.json +8 -4
  30. package/dist/__tests__/cascade.test.d.ts +0 -11
  31. package/dist/__tests__/cascade.test.js +0 -452
  32. package/dist/__tests__/claude.test.d.ts +0 -1
  33. package/dist/__tests__/claude.test.js +0 -427
  34. package/dist/__tests__/custom-loader.test.d.ts +0 -1
  35. package/dist/__tests__/custom-loader.test.js +0 -116
  36. package/dist/__tests__/executor.test.d.ts +0 -1
  37. package/dist/__tests__/executor.test.js +0 -1635
  38. package/dist/__tests__/integration/datadog.integration.test.d.ts +0 -1
  39. package/dist/__tests__/integration/datadog.integration.test.js +0 -23
  40. package/dist/__tests__/integration/e2e-workflow.integration.test.d.ts +0 -1
  41. package/dist/__tests__/integration/e2e-workflow.integration.test.js +0 -75
  42. package/dist/__tests__/integration/github.integration.test.d.ts +0 -1
  43. package/dist/__tests__/integration/github.integration.test.js +0 -37
  44. package/dist/__tests__/integration/harness.d.ts +0 -24
  45. package/dist/__tests__/integration/harness.js +0 -34
  46. package/dist/__tests__/integration/linear.integration.test.d.ts +0 -1
  47. package/dist/__tests__/integration/linear.integration.test.js +0 -15
  48. package/dist/__tests__/integration/sentry.integration.test.d.ts +0 -1
  49. package/dist/__tests__/integration/sentry.integration.test.js +0 -20
  50. package/dist/__tests__/integration/slack.integration.test.d.ts +0 -1
  51. package/dist/__tests__/integration/slack.integration.test.js +0 -22
  52. package/dist/__tests__/integration/sources.integration.test.d.ts +0 -1
  53. package/dist/__tests__/integration/sources.integration.test.js +0 -123
  54. package/dist/__tests__/publish.test.d.ts +0 -1
  55. package/dist/__tests__/publish.test.js +0 -180
  56. package/dist/__tests__/requires.test.d.ts +0 -1
  57. package/dist/__tests__/requires.test.js +0 -82
  58. package/dist/__tests__/retry.test.d.ts +0 -1
  59. package/dist/__tests__/retry.test.js +0 -213
  60. package/dist/__tests__/schema.test.d.ts +0 -1
  61. package/dist/__tests__/schema.test.js +0 -600
  62. package/dist/__tests__/skill-config-integration.test.d.ts +0 -1
  63. package/dist/__tests__/skill-config-integration.test.js +0 -204
  64. package/dist/__tests__/skills-index.test.d.ts +0 -1
  65. package/dist/__tests__/skills-index.test.js +0 -119
  66. package/dist/__tests__/skills.test.d.ts +0 -1
  67. package/dist/__tests__/skills.test.js +0 -565
  68. package/dist/__tests__/spec-conformance.test.d.ts +0 -18
  69. package/dist/__tests__/spec-conformance.test.js +0 -1084
  70. package/dist/__tests__/studio.test.d.ts +0 -1
  71. package/dist/__tests__/studio.test.js +0 -403
  72. package/dist/__tests__/templates.test.d.ts +0 -1
  73. package/dist/__tests__/templates.test.js +0 -49
  74. package/dist/__tests__/testing.test.d.ts +0 -1
  75. package/dist/__tests__/testing.test.js +0 -242
  76. package/dist/__tests__/verify.test.d.ts +0 -1
  77. package/dist/__tests__/verify.test.js +0 -517
  78. package/dist/__tests__/version-consistency.test.d.ts +0 -1
  79. package/dist/__tests__/version-consistency.test.js +0 -312
  80. package/dist/__tests__/workflow-builder.test.d.ts +0 -1
  81. package/dist/__tests__/workflow-builder.test.js +0 -110
  82. package/dist/__tests__/workflow-yaml.test.d.ts +0 -1
  83. package/dist/__tests__/workflow-yaml.test.js +0 -201
  84. package/dist/cli/__tests__/diagram.test.d.ts +0 -1
  85. package/dist/cli/__tests__/diagram.test.js +0 -225
  86. package/dist/cli/__tests__/report-cloud.test.d.ts +0 -1
  87. package/dist/cli/__tests__/report-cloud.test.js +0 -165
  88. package/dist/cli/__tests__/upgrade.test.d.ts +0 -1
  89. package/dist/cli/__tests__/upgrade.test.js +0 -317
  90. package/dist/cli/__tests__/version-check.test.d.ts +0 -1
  91. package/dist/cli/__tests__/version-check.test.js +0 -209
  92. package/dist/cli/config.test.d.ts +0 -1
  93. package/dist/cli/config.test.js +0 -200
  94. package/dist/cli/credentials.test.d.ts +0 -1
  95. package/dist/cli/credentials.test.js +0 -172
  96. package/dist/cli/e2e.test.d.ts +0 -1
  97. package/dist/cli/e2e.test.js +0 -690
  98. package/dist/cli/marketplace.test.d.ts +0 -1
  99. package/dist/cli/marketplace.test.js +0 -355
  100. package/dist/cli/new.test.d.ts +0 -1
  101. package/dist/cli/new.test.js +0 -1043
  102. package/dist/cli/output.test.d.ts +0 -1
  103. package/dist/cli/output.test.js +0 -180
  104. package/dist/cli/publish.test.d.ts +0 -1
  105. package/dist/cli/publish.test.js +0 -610
  106. package/dist/cli/renderer.test.d.ts +0 -1
  107. package/dist/cli/renderer.test.js +0 -302
  108. package/dist/executor.test.d.ts +0 -1
  109. package/dist/executor.test.js +0 -618
  110. package/dist/mcp.test.d.ts +0 -1
  111. package/dist/mcp.test.js +0 -735
  112. package/dist/mermaid.test.d.ts +0 -1
  113. package/dist/mermaid.test.js +0 -256
  114. package/dist/sources.test.d.ts +0 -1
  115. package/dist/sources.test.js +0 -265
  116. package/dist/sources.validation.test.d.ts +0 -12
  117. package/dist/sources.validation.test.js +0 -380
  118. package/dist/workflow-builder.test.d.ts +0 -1
  119. package/dist/workflow-builder.test.js +0 -123
@@ -1,312 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { readFileSync, existsSync } from "node:fs";
3
- import { resolve, relative } from "node:path";
4
- import { execSync } from "node:child_process";
5
- /**
6
- * Version consistency tests — ensure the v5 migration is complete
7
- * and prevent regressions when the action version changes.
8
- *
9
- * These tests validate that:
10
- * 1. No stale v4 action references remain in source or docs
11
- * 2. The README uses dark-mode-safe logo rendering
12
- * 3. The CLI new wizard generates the correct action version
13
- * 4. Docs code blocks don't mix the generic action with triage-only inputs
14
- */
15
- const ROOT = resolve(import.meta.dirname, "..", "..", "..", "..");
16
- const CURRENT_ACTION_VERSION = "v5";
17
- /** The previous action version — split into pieces so git grep doesn't match this file */
18
- const PREV_VERSION = `v${"4"}`;
19
- /** Files that are historical records — allowed to reference old versions */
20
- const HISTORICAL_GLOBS = ["docs/superpowers/", ".tasks/", "node_modules/", ".changeset/", "CHANGELOG"];
21
- function isHistorical(filePath) {
22
- const rel = relative(ROOT, filePath);
23
- return HISTORICAL_GLOBS.some((g) => rel.startsWith(g));
24
- }
25
- /**
26
- * Use git grep for fast, indexed search across the repo.
27
- * Returns array of "file:line:content" strings.
28
- */
29
- function gitGrep(pattern, paths = ["."]) {
30
- try {
31
- const pathArgs = paths.map((p) => `"${p}"`).join(" ");
32
- const result = execSync(`git grep -n "${pattern}" -- ${pathArgs}`, {
33
- cwd: ROOT,
34
- encoding: "utf-8",
35
- maxBuffer: 10 * 1024 * 1024,
36
- });
37
- return result.trim().split("\n").filter(Boolean);
38
- }
39
- catch {
40
- // git grep exits 1 when no matches — that's fine
41
- return [];
42
- }
43
- }
44
- // ─── 1. No stale v4 references ──────────────────────────────────────
45
- describe("action version consistency", () => {
46
- it(`has no swenyai/sweny@${PREV_VERSION} references in source or docs`, () => {
47
- const matches = gitGrep(`swenyai/sweny@${PREV_VERSION}`);
48
- const live = matches.filter((m) => {
49
- const file = resolve(ROOT, m.split(":")[0]);
50
- return !isHistorical(file);
51
- });
52
- if (live.length > 0) {
53
- const summary = live
54
- .map((m) => {
55
- const [f, line] = m.split(":");
56
- return ` ${f}:${line}`;
57
- })
58
- .join("\n");
59
- expect.fail(`Found ${live.length} stale swenyai/sweny@${PREV_VERSION} reference(s):\n${summary}\n\n` +
60
- `All live references should use swenyai/sweny@${CURRENT_ACTION_VERSION}`);
61
- }
62
- });
63
- it("CLAUDE.md references the current action version", () => {
64
- const claude = readFileSync(resolve(ROOT, "CLAUDE.md"), "utf-8");
65
- expect(claude).toContain(`swenyai/sweny@${CURRENT_ACTION_VERSION}`);
66
- expect(claude).not.toContain(`swenyai/sweny@${PREV_VERSION}`);
67
- });
68
- it("CLAUDE.md package table references the current version", () => {
69
- const claude = readFileSync(resolve(ROOT, "CLAUDE.md"), "utf-8");
70
- expect(claude).toMatch(new RegExp(`@sweny-ai/core.*${CURRENT_ACTION_VERSION}`));
71
- });
72
- it("CLI new wizard generates workflows with the current action version", () => {
73
- const newSrc = readFileSync(resolve(ROOT, "packages/core/src/cli/new.ts"), "utf-8");
74
- expect(newSrc).toContain(`swenyai/sweny@${CURRENT_ACTION_VERSION}`);
75
- expect(newSrc).not.toContain(`swenyai/sweny@${PREV_VERSION}`);
76
- });
77
- });
78
- // ─── 2. README quality ──────────────────────────────────────────────
79
- describe("README", () => {
80
- const readme = readFileSync(resolve(ROOT, "README.md"), "utf-8");
81
- it("uses <picture> element for dark mode logo", () => {
82
- expect(readme).toContain("<picture>");
83
- expect(readme).toContain("prefers-color-scheme: dark");
84
- expect(readme).toContain("prefers-color-scheme: light");
85
- });
86
- it("maps dark scheme to light logo and light scheme to dark logo", () => {
87
- // Dark background needs light (white) text logo
88
- expect(readme).toMatch(/prefers-color-scheme:\s*dark.*logo-lockup-light\.svg/s);
89
- // Light background needs dark text logo
90
- expect(readme).toMatch(/prefers-color-scheme:\s*light.*logo-lockup-dark\.svg/s);
91
- });
92
- it("references both logo assets that exist on disk", () => {
93
- expect(existsSync(resolve(ROOT, "assets/logo-lockup-dark.svg"))).toBe(true);
94
- expect(existsSync(resolve(ROOT, "assets/logo-lockup-light.svg"))).toBe(true);
95
- });
96
- it("does not use the old Recipes heading", () => {
97
- expect(readme).not.toMatch(/^## Recipes$/m);
98
- });
99
- it("has a GitHub Actions section with the multi-repo table", () => {
100
- expect(readme).toMatch(/^## GitHub Actions$/m);
101
- expect(readme).toContain("swenyai/triage@v1");
102
- expect(readme).toContain("swenyai/e2e@v1");
103
- expect(readme).toContain(`swenyai/sweny@${CURRENT_ACTION_VERSION}`);
104
- });
105
- });
106
- // ─── 3. action.yml describes a generic workflow runner ──────────────
107
- describe("action.yml", () => {
108
- const actionYml = readFileSync(resolve(ROOT, "action.yml"), "utf-8");
109
- it("has a description under 125 characters", () => {
110
- const match = actionYml.match(/^description:\s*"(.+)"$/m);
111
- expect(match).not.toBeNull();
112
- expect(match[1].length).toBeLessThan(125);
113
- });
114
- it("requires a workflow input", () => {
115
- expect(actionYml).toContain("workflow:");
116
- });
117
- it("does not have triage-specific inputs", () => {
118
- // These belong on swenyai/triage, not the generic runner
119
- const triageInputs = [
120
- "observability-provider",
121
- "sentry-auth-token",
122
- "dd-api-key",
123
- "dd-app-key",
124
- "issue-tracker-provider",
125
- "linear-api-key",
126
- "dry-run",
127
- "time-range",
128
- "severity-focus",
129
- ];
130
- for (const input of triageInputs) {
131
- expect(actionYml).not.toContain(`${input}:`);
132
- }
133
- });
134
- it("runs sweny workflow run", () => {
135
- expect(actionYml).toMatch(/sweny workflow run/);
136
- });
137
- });
138
- // ─── 4. Docs code blocks don't mix generic action with triage inputs ─
139
- describe("docs action code blocks", () => {
140
- const TRIAGE_ONLY_INPUTS = [
141
- "observability-provider",
142
- "sentry-auth-token",
143
- "sentry-org",
144
- "sentry-project",
145
- "dd-api-key",
146
- "dd-app-key",
147
- "issue-tracker-provider",
148
- "linear-api-key",
149
- "linear-team-id",
150
- "dry-run:",
151
- "time-range",
152
- "severity-focus",
153
- "service-filter",
154
- "investigation-depth",
155
- ];
156
- /**
157
- * Extract YAML code blocks that reference swenyai/sweny@v5
158
- * and check they don't use triage-only inputs.
159
- */
160
- function findMisusedBlocks(filePath) {
161
- if (!existsSync(filePath))
162
- return [];
163
- const content = readFileSync(filePath, "utf-8");
164
- const blocks = content.match(/```ya?ml[\s\S]*?```/g) || [];
165
- const issues = [];
166
- for (const block of blocks) {
167
- if (!block.includes(`swenyai/sweny@${CURRENT_ACTION_VERSION}`))
168
- continue;
169
- for (const input of TRIAGE_ONLY_INPUTS) {
170
- if (block.includes(input)) {
171
- issues.push(`${relative(ROOT, filePath)}: code block uses swenyai/sweny@${CURRENT_ACTION_VERSION} with triage-only input "${input}"`);
172
- break; // one issue per block is enough
173
- }
174
- }
175
- }
176
- return issues;
177
- }
178
- // These are the docs files that show GitHub Action examples
179
- const ACTION_DOC_FILES = [
180
- "packages/web/src/content/docs/action/index.md",
181
- "packages/web/src/content/docs/action/examples.md",
182
- "packages/web/src/content/docs/action/scheduling.md",
183
- "packages/web/src/content/docs/action/inputs.md",
184
- "packages/web/src/content/docs/action/service-map.md",
185
- "packages/web/src/content/docs/index.mdx",
186
- "packages/web/src/content/docs/getting-started/quick-start.md",
187
- "packages/web/src/content/docs/workflows/triage.md",
188
- "packages/web/src/content/docs/workflows/implement.md",
189
- "packages/web/src/content/docs/cloud/getting-started.mdx",
190
- "packages/web/src/content/docs/advanced/mcp-servers.md",
191
- "packages/web/src/content/docs/advanced/troubleshooting.md",
192
- ];
193
- it("no docs code block uses swenyai/sweny@v5 with triage-only inputs", () => {
194
- const allIssues = ACTION_DOC_FILES.flatMap((f) => findMisusedBlocks(resolve(ROOT, f)));
195
- if (allIssues.length > 0) {
196
- expect.fail(`Found ${allIssues.length} code block(s) mixing swenyai/sweny@${CURRENT_ACTION_VERSION} with triage-only inputs:\n` +
197
- allIssues.map((i) => ` ${i}`).join("\n"));
198
- }
199
- });
200
- it("every swenyai/sweny@v5 code block has a workflow: input", () => {
201
- const issues = [];
202
- for (const f of ACTION_DOC_FILES) {
203
- const filePath = resolve(ROOT, f);
204
- if (!existsSync(filePath))
205
- continue;
206
- const content = readFileSync(filePath, "utf-8");
207
- const blocks = content.match(/```ya?ml[\s\S]*?```/g) || [];
208
- for (const block of blocks) {
209
- if (!block.includes(`swenyai/sweny@${CURRENT_ACTION_VERSION}`))
210
- continue;
211
- if (!block.includes("workflow:")) {
212
- issues.push(`${relative(ROOT, filePath)}: code block uses swenyai/sweny@${CURRENT_ACTION_VERSION} without workflow: input`);
213
- }
214
- }
215
- }
216
- if (issues.length > 0) {
217
- expect.fail(`The generic runner requires a workflow: input.\n` + issues.map((i) => ` ${i}`).join("\n"));
218
- }
219
- });
220
- });
221
- // ─── 5. Internal doc links resolve ──────────────────────────────────
222
- describe("docs internal links", () => {
223
- const DOCS_ROOT = resolve(ROOT, "packages/web/src/content/docs");
224
- /**
225
- * Extract markdown links like [text](/path/) and verify the target exists.
226
- * Only checks internal absolute links (starting with /).
227
- */
228
- function findBrokenLinks(filePath) {
229
- if (!existsSync(filePath))
230
- return [];
231
- const content = readFileSync(filePath, "utf-8");
232
- const broken = [];
233
- // Match [text](/path/) and [text](/path/#anchor) but skip external URLs
234
- const linkPattern = /\[([^\]]*)\]\(\/([^)#]*?)\/?\)/g;
235
- let match;
236
- while ((match = linkPattern.exec(content)) !== null) {
237
- const linkPath = match[2];
238
- // Map /action/ → action/index.md, /action/inputs/ → action/inputs.md
239
- const candidates = [
240
- resolve(DOCS_ROOT, linkPath, "index.md"),
241
- resolve(DOCS_ROOT, linkPath, "index.mdx"),
242
- resolve(DOCS_ROOT, `${linkPath}.md`),
243
- resolve(DOCS_ROOT, `${linkPath}.mdx`),
244
- ];
245
- if (!candidates.some((c) => existsSync(c))) {
246
- broken.push(`${relative(ROOT, filePath)}: broken link [${match[1]}](/${linkPath}/)`);
247
- }
248
- }
249
- return broken;
250
- }
251
- const KEY_DOCS = [
252
- "packages/web/src/content/docs/action/index.md",
253
- "packages/web/src/content/docs/action/inputs.md",
254
- "packages/web/src/content/docs/action/examples.md",
255
- "packages/web/src/content/docs/action/scheduling.md",
256
- "packages/web/src/content/docs/action/service-map.md",
257
- "packages/web/src/content/docs/index.mdx",
258
- "packages/web/src/content/docs/getting-started/quick-start.md",
259
- ];
260
- it("key action docs have no broken internal links", () => {
261
- const allBroken = KEY_DOCS.flatMap((f) => findBrokenLinks(resolve(ROOT, f)));
262
- if (allBroken.length > 0) {
263
- expect.fail(`Found ${allBroken.length} broken link(s):\n` + allBroken.map((b) => ` ${b}`).join("\n"));
264
- }
265
- });
266
- });
267
- // ─── 6. Three-action architecture documented ────────────────────────
268
- describe("three-action architecture", () => {
269
- it("action/index.md documents all three actions", () => {
270
- const content = readFileSync(resolve(ROOT, "packages/web/src/content/docs/action/index.md"), "utf-8");
271
- expect(content).toContain("swenyai/triage@v1");
272
- expect(content).toContain("swenyai/e2e@v1");
273
- expect(content).toContain(`swenyai/sweny@${CURRENT_ACTION_VERSION}`);
274
- });
275
- it("action/inputs.md has sections for all three actions", () => {
276
- const content = readFileSync(resolve(ROOT, "packages/web/src/content/docs/action/inputs.md"), "utf-8");
277
- expect(content).toMatch(/## Generic runner inputs/);
278
- expect(content).toMatch(/## Triage action inputs/);
279
- expect(content).toMatch(/## E2E action inputs/);
280
- });
281
- it("README Actions table lists all three actions", () => {
282
- const readme = readFileSync(resolve(ROOT, "README.md"), "utf-8");
283
- expect(readme).toContain("swenyai/triage@v1");
284
- expect(readme).toContain("swenyai/e2e@v1");
285
- expect(readme).toContain(`swenyai/sweny@${CURRENT_ACTION_VERSION}`);
286
- });
287
- });
288
- // ─── 7. Input naming consistency ────────────────────────────────────
289
- describe("input naming consistency", () => {
290
- const DOCS_DIR = resolve(ROOT, "packages/web/src/content/docs");
291
- it("docs use issue-override, not linear-issue", () => {
292
- const matches = gitGrep("linear-issue", ["packages/web/src/content/docs/"]);
293
- if (matches.length > 0) {
294
- expect.fail(`Found "linear-issue" in docs — the triage action input is "issue-override":\n` +
295
- matches.map((m) => ` ${m.split(":").slice(0, 2).join(":")}`).join("\n"));
296
- }
297
- });
298
- it("docs use mcp-servers-json, not bare mcp-servers as an input name", () => {
299
- // Match `mcp-servers:` or `mcp-servers |` in YAML (the input name), but NOT
300
- // `mcp-servers-json` or prose text mentioning "MCP servers" as a concept
301
- const matches = gitGrep("mcp-servers[^-]", ["packages/web/src/content/docs/"]);
302
- const inputRefs = matches.filter((m) => {
303
- const content = m.split(":").slice(2).join(":");
304
- // Only flag YAML input usage, not prose text
305
- return /^\s*(mcp-servers:|mcp-servers\s*\|)/.test(content);
306
- });
307
- if (inputRefs.length > 0) {
308
- expect.fail(`Found bare "mcp-servers" as an input — the triage action input is "mcp-servers-json":\n` +
309
- inputRefs.map((m) => ` ${m.split(":").slice(0, 2).join(":")}`).join("\n"));
310
- }
311
- });
312
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,110 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { buildSystemPrompt, deriveWorkflowVariables } from "../workflow-builder.js";
3
- const fakeSkill = (id, config = {}) => ({
4
- id,
5
- name: id,
6
- description: `${id} skill`,
7
- category: "general",
8
- config,
9
- tools: [],
10
- });
11
- describe("buildSystemPrompt", () => {
12
- it("includes config fields in the skill list", () => {
13
- const skills = [
14
- fakeSkill("github", {
15
- GITHUB_TOKEN: { description: "GitHub token", required: true, env: "GITHUB_TOKEN" },
16
- }),
17
- ];
18
- const prompt = buildSystemPrompt(skills);
19
- expect(prompt).toContain("- github: github skill");
20
- expect(prompt).toContain("Config: GITHUB_TOKEN (required)");
21
- });
22
- it("shows optional config fields", () => {
23
- const skills = [
24
- fakeSkill("slack", {
25
- SLACK_WEBHOOK_URL: { description: "Webhook URL", required: false, env: "SLACK_WEBHOOK_URL" },
26
- SLACK_BOT_TOKEN: { description: "Bot token", required: false, env: "SLACK_BOT_TOKEN" },
27
- }),
28
- ];
29
- const prompt = buildSystemPrompt(skills);
30
- expect(prompt).toContain("SLACK_WEBHOOK_URL (optional)");
31
- expect(prompt).toContain("SLACK_BOT_TOKEN (optional)");
32
- });
33
- it("omits Config line for skills with no config", () => {
34
- const skills = [fakeSkill("simple")];
35
- const prompt = buildSystemPrompt(skills);
36
- expect(prompt).toContain("- simple: simple skill");
37
- expect(prompt).not.toContain("Config:");
38
- });
39
- });
40
- describe("deriveWorkflowVariables", () => {
41
- const workflow = {
42
- id: "test",
43
- name: "test",
44
- description: "test workflow",
45
- entry: "gather",
46
- nodes: {
47
- gather: {
48
- name: "Gather",
49
- instruction: "gather data",
50
- skills: ["github", "slack"],
51
- },
52
- },
53
- edges: [],
54
- };
55
- it("always includes ANTHROPIC_API_KEY first", () => {
56
- const vars = deriveWorkflowVariables(workflow, []);
57
- expect(vars[0].name).toBe("ANTHROPIC_API_KEY");
58
- expect(vars[0].required).toBe(true);
59
- });
60
- it("derives variables from referenced skills", () => {
61
- const skills = [
62
- fakeSkill("github", {
63
- GITHUB_TOKEN: { description: "GitHub token", required: true, env: "GITHUB_TOKEN" },
64
- }),
65
- fakeSkill("slack", {
66
- SLACK_WEBHOOK_URL: { description: "Webhook URL", required: false, env: "SLACK_WEBHOOK_URL" },
67
- }),
68
- ];
69
- const vars = deriveWorkflowVariables(workflow, skills);
70
- const names = vars.map((v) => v.name);
71
- expect(names).toContain("GITHUB_TOKEN");
72
- expect(names).toContain("SLACK_WEBHOOK_URL");
73
- });
74
- it("deduplicates env vars across skills", () => {
75
- const multiNodeWorkflow = {
76
- ...workflow,
77
- nodes: {
78
- a: { name: "A", instruction: "a", skills: ["github"] },
79
- b: { name: "B", instruction: "b", skills: ["github"] },
80
- },
81
- };
82
- const skills = [
83
- fakeSkill("github", {
84
- GITHUB_TOKEN: { description: "GitHub token", required: true, env: "GITHUB_TOKEN" },
85
- }),
86
- ];
87
- const vars = deriveWorkflowVariables(multiNodeWorkflow, skills);
88
- const tokenCount = vars.filter((v) => v.name === "GITHUB_TOKEN").length;
89
- expect(tokenCount).toBe(1);
90
- });
91
- it("skips skills not in the available list", () => {
92
- const vars = deriveWorkflowVariables(workflow, []);
93
- // Only ANTHROPIC_API_KEY
94
- expect(vars).toHaveLength(1);
95
- });
96
- it("includes custom skills with config", () => {
97
- const customWorkflow = {
98
- ...workflow,
99
- nodes: { tax: { name: "Tax", instruction: "do taxes", skills: ["my-tax-tool"] } },
100
- };
101
- const skills = [
102
- fakeSkill("my-tax-tool", {
103
- MY_TAX_KEY: { description: "Tax API key", required: true, env: "MY_TAX_KEY" },
104
- }),
105
- ];
106
- const vars = deriveWorkflowVariables(customWorkflow, skills);
107
- expect(vars.find((v) => v.name === "MY_TAX_KEY")).toBeDefined();
108
- expect(vars.find((v) => v.name === "MY_TAX_KEY")?.skill).toBe("my-tax-tool");
109
- });
110
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,201 +0,0 @@
1
- /**
2
- * Tests for YAML-based built-in workflows.
3
- *
4
- * Validates that the YAML files are correctly loaded, structurally valid,
5
- * match the Workflow type, and that the browser bundle is consistent.
6
- */
7
- import { describe, it, expect } from "vitest";
8
- import * as fs from "node:fs";
9
- import * as path from "node:path";
10
- import { fileURLToPath } from "node:url";
11
- import { parse as parseYaml } from "yaml";
12
- import { triageWorkflow, implementWorkflow, seedContentWorkflow } from "../workflows/index.js";
13
- import { validateWorkflow, workflowZ } from "../schema.js";
14
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
- const workflowDir = path.join(__dirname, "..", "workflows");
16
- // ─── YAML file existence & parsing ──────────────────────────────
17
- describe("workflow YAML files", () => {
18
- const yamlFiles = ["triage.yml", "implement.yml", "seed-content.yml"];
19
- for (const file of yamlFiles) {
20
- it(`${file} exists on disk`, () => {
21
- const filePath = path.join(workflowDir, file);
22
- expect(fs.existsSync(filePath)).toBe(true);
23
- });
24
- it(`${file} is valid YAML`, () => {
25
- const content = fs.readFileSync(path.join(workflowDir, file), "utf-8");
26
- expect(() => parseYaml(content)).not.toThrow();
27
- });
28
- it(`${file} parses to an object with required Workflow fields`, () => {
29
- const content = fs.readFileSync(path.join(workflowDir, file), "utf-8");
30
- const data = parseYaml(content);
31
- expect(data).toHaveProperty("id");
32
- expect(data).toHaveProperty("name");
33
- expect(data).toHaveProperty("entry");
34
- expect(data).toHaveProperty("nodes");
35
- expect(data).toHaveProperty("edges");
36
- expect(typeof data.id).toBe("string");
37
- expect(typeof data.name).toBe("string");
38
- expect(typeof data.entry).toBe("string");
39
- expect(typeof data.nodes).toBe("object");
40
- expect(Array.isArray(data.edges)).toBe(true);
41
- });
42
- it(`${file} passes Zod schema validation`, () => {
43
- const content = fs.readFileSync(path.join(workflowDir, file), "utf-8");
44
- const data = parseYaml(content);
45
- expect(() => workflowZ.parse(data)).not.toThrow();
46
- });
47
- }
48
- });
49
- // ─── Loaded workflow objects ────────────────────────────────────
50
- describe("loaded workflow objects", () => {
51
- const workflows = [
52
- ["triage", triageWorkflow],
53
- ["implement", implementWorkflow],
54
- ["seed-content", seedContentWorkflow],
55
- ];
56
- for (const [name, workflow] of workflows) {
57
- describe(name, () => {
58
- it("has correct id", () => {
59
- expect(workflow.id).toBe(name);
60
- });
61
- it("has a non-empty name", () => {
62
- expect(workflow.name.length).toBeGreaterThan(0);
63
- });
64
- it("entry node exists in nodes", () => {
65
- expect(workflow.nodes).toHaveProperty(workflow.entry);
66
- });
67
- it("passes structural validation (no cycles, all reachable)", () => {
68
- const errors = validateWorkflow(workflow);
69
- expect(errors).toEqual([]);
70
- });
71
- it("every node has a non-empty instruction", () => {
72
- for (const [nodeId, node] of Object.entries(workflow.nodes)) {
73
- const instr = node.instruction;
74
- if (typeof instr === "string") {
75
- expect(instr.length, `node "${nodeId}" has empty instruction`).toBeGreaterThan(0);
76
- }
77
- else {
78
- // Object-form Source — always truthy if it exists
79
- expect(instr, `node "${nodeId}" has falsy instruction`).toBeTruthy();
80
- }
81
- }
82
- });
83
- it("every node has a name", () => {
84
- for (const [nodeId, node] of Object.entries(workflow.nodes)) {
85
- expect(node.name.length, `node "${nodeId}" has empty name`).toBeGreaterThan(0);
86
- }
87
- });
88
- it("all edge sources reference existing nodes", () => {
89
- const nodeIds = new Set(Object.keys(workflow.nodes));
90
- for (const edge of workflow.edges) {
91
- expect(nodeIds.has(edge.from), `edge source "${edge.from}" not in nodes`).toBe(true);
92
- }
93
- });
94
- it("all edge targets reference existing nodes", () => {
95
- const nodeIds = new Set(Object.keys(workflow.nodes));
96
- for (const edge of workflow.edges) {
97
- expect(nodeIds.has(edge.to), `edge target "${edge.to}" not in nodes`).toBe(true);
98
- }
99
- });
100
- it("has no self-loops", () => {
101
- for (const edge of workflow.edges) {
102
- expect(edge.from, `self-loop on "${edge.from}"`).not.toBe(edge.to);
103
- }
104
- });
105
- it("all non-terminal nodes have at least one outgoing edge", () => {
106
- const nodeIds = Object.keys(workflow.nodes);
107
- const nodesWithOutgoing = new Set(workflow.edges.map((e) => e.from));
108
- const terminalNodes = nodeIds.filter((id) => !nodesWithOutgoing.has(id));
109
- // Terminal nodes are fine (they're leaf nodes), but there should be at least one
110
- // non-terminal node unless it's a single-node workflow
111
- if (nodeIds.length > 1) {
112
- expect(nodesWithOutgoing.size).toBeGreaterThan(0);
113
- }
114
- });
115
- });
116
- }
117
- });
118
- // ─── Triage workflow specifics ──────────────────────────────────
119
- describe("triage workflow specifics", () => {
120
- it("has 7 nodes", () => {
121
- expect(Object.keys(triageWorkflow.nodes).length).toBe(7);
122
- });
123
- it("starts at gather", () => {
124
- expect(triageWorkflow.entry).toBe("gather");
125
- });
126
- it("has conditional edges from investigate", () => {
127
- const investigateEdges = triageWorkflow.edges.filter((e) => e.from === "investigate");
128
- expect(investigateEdges.length).toBe(2);
129
- expect(investigateEdges.every((e) => e.when)).toBe(true);
130
- });
131
- it("notify is reachable from all branches", () => {
132
- const toNotify = triageWorkflow.edges.filter((e) => e.to === "notify");
133
- expect(toNotify.length).toBeGreaterThanOrEqual(3);
134
- });
135
- it("investigate node has structured output schema", () => {
136
- expect(triageWorkflow.nodes.investigate.output).toBeDefined();
137
- expect(triageWorkflow.nodes.investigate.output.type).toBe("object");
138
- expect(triageWorkflow.nodes.investigate.output.required).toContain("findings");
139
- });
140
- });
141
- // ─── Implement workflow specifics ───────────────────────────────
142
- describe("implement workflow specifics", () => {
143
- it("has 5 nodes", () => {
144
- expect(Object.keys(implementWorkflow.nodes).length).toBe(5);
145
- });
146
- it("starts at analyze", () => {
147
- expect(implementWorkflow.entry).toBe("analyze");
148
- });
149
- it("has conditional edges from analyze", () => {
150
- const analyzeEdges = implementWorkflow.edges.filter((e) => e.from === "analyze");
151
- expect(analyzeEdges.length).toBe(2);
152
- expect(analyzeEdges.every((e) => e.when)).toBe(true);
153
- });
154
- it("skip routes to notify (team always gets notified)", () => {
155
- const skipToNotify = implementWorkflow.edges.find((e) => e.from === "skip" && e.to === "notify");
156
- expect(skipToNotify).toBeDefined();
157
- });
158
- it("analyze node has structured output schema", () => {
159
- expect(implementWorkflow.nodes.analyze.output).toBeDefined();
160
- expect(implementWorkflow.nodes.analyze.output.type).toBe("object");
161
- });
162
- });
163
- // ─── Browser bundle consistency ─────────────────────────────────
164
- describe("browser bundle", () => {
165
- const browserPath = path.join(__dirname, "..", "..", "dist", "workflows", "browser.js");
166
- it("exists in dist", () => {
167
- expect(fs.existsSync(browserPath)).toBe(true);
168
- });
169
- it("triage workflow is deeply equal", async () => {
170
- const browser = await import(browserPath);
171
- expect(browser.triageWorkflow).toEqual(JSON.parse(JSON.stringify(triageWorkflow)));
172
- });
173
- it("implement workflow is deeply equal", async () => {
174
- const browser = await import(browserPath);
175
- expect(browser.implementWorkflow).toEqual(JSON.parse(JSON.stringify(implementWorkflow)));
176
- });
177
- it("seed-content workflow is deeply equal", async () => {
178
- const browser = await import(browserPath);
179
- expect(browser.seedContentWorkflow).toEqual(JSON.parse(JSON.stringify(seedContentWorkflow)));
180
- });
181
- it("exports exactly the expected workflow names", async () => {
182
- const browser = await import(browserPath);
183
- const exportedKeys = Object.keys(browser).sort();
184
- expect(exportedKeys).toEqual(["implementWorkflow", "seedContentWorkflow", "triageWorkflow"]);
185
- });
186
- });
187
- // ─── Zod runtime validation ─────────────────────────────────────
188
- describe("runtime Zod validation in loader", () => {
189
- it("workflows are Zod-validated at load time (not just cast)", () => {
190
- // The loader uses workflowZ.parse(), so invalid YAML would throw at import.
191
- // If we got here, all 3 workflows passed Zod validation at module load.
192
- // Verify by checking the parsed output has Zod's default-applied fields.
193
- expect(triageWorkflow.description).toBeDefined();
194
- expect(implementWorkflow.description).toBeDefined();
195
- expect(seedContentWorkflow.description).toBeDefined();
196
- // Zod applies .default([]) to skills arrays — verify it works
197
- for (const node of Object.values(triageWorkflow.nodes)) {
198
- expect(Array.isArray(node.skills)).toBe(true);
199
- }
200
- });
201
- });
@@ -1 +0,0 @@
1
- export {};