@sweny-ai/core 0.1.76 → 0.1.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser.d.ts +11 -5
- package/dist/browser.js +11 -11
- package/dist/claude.d.ts +20 -0
- package/dist/claude.js +139 -9
- package/dist/cli/config.d.ts +22 -0
- package/dist/cli/config.js +56 -9
- package/dist/cli/main.d.ts +1 -1
- package/dist/cli/main.js +56 -57
- package/dist/cli/marketplace.js +35 -9
- package/dist/cli/publish.d.ts +10 -0
- package/dist/cli/publish.js +22 -14
- package/dist/executor.js +104 -43
- package/dist/loader.d.ts +40 -0
- package/dist/loader.js +73 -0
- package/dist/mcp-catalog.d.ts +47 -0
- package/dist/mcp-catalog.js +304 -0
- package/dist/mcp.d.ts +6 -9
- package/dist/mcp.js +24 -314
- package/dist/requires.d.ts +7 -0
- package/dist/requires.js +34 -0
- package/dist/retry.d.ts +23 -0
- package/dist/retry.js +72 -0
- package/dist/schema.d.ts +667 -80
- package/dist/schema.js +144 -5
- package/dist/skills/custom-loader.d.ts +40 -0
- package/dist/skills/custom-loader.js +107 -32
- package/dist/skills/index.d.ts +13 -1
- package/dist/skills/index.js +12 -3
- package/dist/studio.js +4 -1
- package/dist/templates.d.ts +29 -8
- package/dist/templates.js +30 -20
- package/dist/testing.d.ts +7 -0
- package/dist/testing.js +7 -0
- package/dist/types.d.ts +76 -0
- package/dist/verify.d.ts +1 -0
- package/dist/verify.js +43 -5
- package/package.json +8 -4
- package/dist/__tests__/cascade.test.d.ts +0 -11
- package/dist/__tests__/cascade.test.js +0 -449
- package/dist/__tests__/claude.test.d.ts +0 -1
- package/dist/__tests__/claude.test.js +0 -427
- package/dist/__tests__/custom-loader.test.d.ts +0 -1
- package/dist/__tests__/custom-loader.test.js +0 -116
- package/dist/__tests__/executor.test.d.ts +0 -1
- package/dist/__tests__/executor.test.js +0 -736
- package/dist/__tests__/integration/datadog.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/datadog.integration.test.js +0 -23
- package/dist/__tests__/integration/e2e-workflow.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/e2e-workflow.integration.test.js +0 -75
- package/dist/__tests__/integration/github.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/github.integration.test.js +0 -37
- package/dist/__tests__/integration/harness.d.ts +0 -24
- package/dist/__tests__/integration/harness.js +0 -34
- package/dist/__tests__/integration/linear.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/linear.integration.test.js +0 -15
- package/dist/__tests__/integration/sentry.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/sentry.integration.test.js +0 -20
- package/dist/__tests__/integration/slack.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/slack.integration.test.js +0 -22
- package/dist/__tests__/integration/sources.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/sources.integration.test.js +0 -123
- package/dist/__tests__/publish.test.d.ts +0 -1
- package/dist/__tests__/publish.test.js +0 -180
- package/dist/__tests__/schema.test.d.ts +0 -1
- package/dist/__tests__/schema.test.js +0 -526
- package/dist/__tests__/skill-config-integration.test.d.ts +0 -1
- package/dist/__tests__/skill-config-integration.test.js +0 -204
- package/dist/__tests__/skills-index.test.d.ts +0 -1
- package/dist/__tests__/skills-index.test.js +0 -119
- package/dist/__tests__/skills.test.d.ts +0 -1
- package/dist/__tests__/skills.test.js +0 -565
- package/dist/__tests__/spec-conformance.test.d.ts +0 -18
- package/dist/__tests__/spec-conformance.test.js +0 -1074
- package/dist/__tests__/studio.test.d.ts +0 -1
- package/dist/__tests__/studio.test.js +0 -403
- package/dist/__tests__/templates.test.d.ts +0 -1
- package/dist/__tests__/templates.test.js +0 -49
- package/dist/__tests__/testing.test.d.ts +0 -1
- package/dist/__tests__/testing.test.js +0 -224
- package/dist/__tests__/verify.test.d.ts +0 -1
- package/dist/__tests__/verify.test.js +0 -517
- package/dist/__tests__/version-consistency.test.d.ts +0 -1
- package/dist/__tests__/version-consistency.test.js +0 -312
- package/dist/__tests__/workflow-builder.test.d.ts +0 -1
- package/dist/__tests__/workflow-builder.test.js +0 -110
- package/dist/__tests__/workflow-yaml.test.d.ts +0 -1
- package/dist/__tests__/workflow-yaml.test.js +0 -201
- package/dist/cli/__tests__/diagram.test.d.ts +0 -1
- package/dist/cli/__tests__/diagram.test.js +0 -225
- package/dist/cli/__tests__/report-cloud.test.d.ts +0 -1
- package/dist/cli/__tests__/report-cloud.test.js +0 -165
- package/dist/cli/__tests__/upgrade.test.d.ts +0 -1
- package/dist/cli/__tests__/upgrade.test.js +0 -317
- package/dist/cli/__tests__/version-check.test.d.ts +0 -1
- package/dist/cli/__tests__/version-check.test.js +0 -209
- package/dist/cli/config.test.d.ts +0 -1
- package/dist/cli/config.test.js +0 -200
- package/dist/cli/credentials.test.d.ts +0 -1
- package/dist/cli/credentials.test.js +0 -172
- package/dist/cli/e2e.test.d.ts +0 -1
- package/dist/cli/e2e.test.js +0 -690
- package/dist/cli/marketplace.test.d.ts +0 -1
- package/dist/cli/marketplace.test.js +0 -355
- package/dist/cli/new.test.d.ts +0 -1
- package/dist/cli/new.test.js +0 -1043
- package/dist/cli/output.test.d.ts +0 -1
- package/dist/cli/output.test.js +0 -180
- package/dist/cli/publish.test.d.ts +0 -1
- package/dist/cli/publish.test.js +0 -610
- package/dist/cli/renderer.test.d.ts +0 -1
- package/dist/cli/renderer.test.js +0 -302
- package/dist/executor.test.d.ts +0 -1
- package/dist/executor.test.js +0 -603
- package/dist/mcp.test.d.ts +0 -1
- package/dist/mcp.test.js +0 -735
- package/dist/mermaid.test.d.ts +0 -1
- package/dist/mermaid.test.js +0 -256
- package/dist/sources.test.d.ts +0 -1
- package/dist/sources.test.js +0 -265
- package/dist/sources.validation.test.d.ts +0 -12
- package/dist/sources.validation.test.js +0 -380
- package/dist/workflow-builder.test.d.ts +0 -1
- package/dist/workflow-builder.test.js +0 -117
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { available } from "./harness.js";
|
|
3
|
-
import { datadog } from "../../skills/datadog.js";
|
|
4
|
-
const ctx = () => ({
|
|
5
|
-
config: {
|
|
6
|
-
DD_API_KEY: process.env.DD_API_KEY,
|
|
7
|
-
DD_APP_KEY: process.env.DD_APP_KEY,
|
|
8
|
-
DD_SITE: process.env.DD_SITE ?? "",
|
|
9
|
-
},
|
|
10
|
-
logger: console,
|
|
11
|
-
});
|
|
12
|
-
describe.runIf(available.datadog)("datadog integration", () => {
|
|
13
|
-
it("lists monitors", async () => {
|
|
14
|
-
const tool = datadog.tools.find((t) => t.name === "datadog_list_monitors");
|
|
15
|
-
const result = await tool.handler({}, ctx());
|
|
16
|
-
expect(Array.isArray(result)).toBe(true);
|
|
17
|
-
}, 15_000);
|
|
18
|
-
it("searches logs", async () => {
|
|
19
|
-
const tool = datadog.tools.find((t) => t.name === "datadog_search_logs");
|
|
20
|
-
const result = await tool.handler({ query: "*", from: "now-5m", limit: 5 }, ctx());
|
|
21
|
-
expect(result).toHaveProperty("data");
|
|
22
|
-
}, 15_000);
|
|
23
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { available, logAvailability } from "./harness.js";
|
|
3
|
-
import { execute } from "../../executor.js";
|
|
4
|
-
import { ClaudeClient } from "../../claude.js";
|
|
5
|
-
import { createSkillMap } from "../../skills/index.js";
|
|
6
|
-
logAvailability();
|
|
7
|
-
// A minimal 2-node workflow that only uses Claude (no external tools)
|
|
8
|
-
const analysisWorkflow = {
|
|
9
|
-
id: "e2e-analysis",
|
|
10
|
-
name: "E2E Analysis",
|
|
11
|
-
description: "Two-node workflow using real Claude",
|
|
12
|
-
entry: "analyze",
|
|
13
|
-
nodes: {
|
|
14
|
-
analyze: {
|
|
15
|
-
name: "Analyze Input",
|
|
16
|
-
instruction: "Analyze the input data and determine if it represents a critical issue. Return JSON with fields: severity (high/medium/low) and summary (one sentence).",
|
|
17
|
-
skills: [],
|
|
18
|
-
output: {
|
|
19
|
-
type: "object",
|
|
20
|
-
properties: {
|
|
21
|
-
severity: { type: "string", enum: ["high", "medium", "low"] },
|
|
22
|
-
summary: { type: "string" },
|
|
23
|
-
},
|
|
24
|
-
required: ["severity", "summary"],
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
report: {
|
|
28
|
-
name: "Generate Report",
|
|
29
|
-
instruction: "Based on the analysis, write a brief incident report. Return JSON with fields: title (string) and recommendation (string).",
|
|
30
|
-
skills: [],
|
|
31
|
-
output: {
|
|
32
|
-
type: "object",
|
|
33
|
-
properties: {
|
|
34
|
-
title: { type: "string" },
|
|
35
|
-
recommendation: { type: "string" },
|
|
36
|
-
},
|
|
37
|
-
required: ["title", "recommendation"],
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
},
|
|
41
|
-
edges: [{ from: "analyze", to: "report" }],
|
|
42
|
-
};
|
|
43
|
-
describe.runIf(available.claude)("e2e workflow with real Claude", () => {
|
|
44
|
-
it("executes a 2-node analysis workflow", async () => {
|
|
45
|
-
const claude = new ClaudeClient({
|
|
46
|
-
model: "claude-haiku-4-5-20251001", // Use Haiku for speed/cost in tests
|
|
47
|
-
maxTurns: 3,
|
|
48
|
-
});
|
|
49
|
-
const events = [];
|
|
50
|
-
const { results } = await execute(analysisWorkflow, {
|
|
51
|
-
alert: "CPU usage at 98% for 15 minutes on production server web-03",
|
|
52
|
-
service: "api-gateway",
|
|
53
|
-
timestamp: "2025-03-25T08:00:00Z",
|
|
54
|
-
}, {
|
|
55
|
-
skills: createSkillMap([]),
|
|
56
|
-
claude,
|
|
57
|
-
observer: (e) => events.push(e),
|
|
58
|
-
config: {},
|
|
59
|
-
});
|
|
60
|
-
// Both nodes executed
|
|
61
|
-
expect(results.size).toBe(2);
|
|
62
|
-
expect(results.get("analyze")?.status).toBe("success");
|
|
63
|
-
expect(results.get("report")?.status).toBe("success");
|
|
64
|
-
// Analysis should have severity
|
|
65
|
-
const analysis = results.get("analyze").data;
|
|
66
|
-
expect(analysis).toBeDefined();
|
|
67
|
-
// Report should have title
|
|
68
|
-
const report = results.get("report").data;
|
|
69
|
-
expect(report).toBeDefined();
|
|
70
|
-
// Events stream should be complete
|
|
71
|
-
expect(events[0].type).toBe("workflow:start");
|
|
72
|
-
expect(events[events.length - 1].type).toBe("workflow:end");
|
|
73
|
-
expect(events.filter((e) => e.type === "node:enter")).toHaveLength(2);
|
|
74
|
-
}, 60_000); // 60s timeout for real API calls
|
|
75
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { available, logAvailability } from "./harness.js";
|
|
3
|
-
import { github } from "../../skills/github.js";
|
|
4
|
-
logAvailability();
|
|
5
|
-
const ctx = () => ({
|
|
6
|
-
config: { GITHUB_TOKEN: process.env.GITHUB_TOKEN },
|
|
7
|
-
logger: console,
|
|
8
|
-
});
|
|
9
|
-
describe.runIf(available.github)("github integration", () => {
|
|
10
|
-
it("searches code in a public repo", async () => {
|
|
11
|
-
const tool = github.tools.find((t) => t.name === "github_search_code");
|
|
12
|
-
const result = await tool.handler({ query: "README", repo: "octocat/Hello-World" }, ctx());
|
|
13
|
-
expect(result).toHaveProperty("items");
|
|
14
|
-
}, 15_000);
|
|
15
|
-
it("gets an issue from a public repo", async () => {
|
|
16
|
-
const tool = github.tools.find((t) => t.name === "github_get_issue");
|
|
17
|
-
const result = await tool.handler({ repo: "octocat/Hello-World", number: 1 }, ctx());
|
|
18
|
-
expect(result).toHaveProperty("title");
|
|
19
|
-
expect(result).toHaveProperty("state");
|
|
20
|
-
}, 15_000);
|
|
21
|
-
it("lists recent commits", async () => {
|
|
22
|
-
const tool = github.tools.find((t) => t.name === "github_list_recent_commits");
|
|
23
|
-
const result = await tool.handler({ repo: "octocat/Hello-World", branch: "master", per_page: 3 }, ctx());
|
|
24
|
-
expect(Array.isArray(result)).toBe(true);
|
|
25
|
-
expect(result.length).toBeGreaterThan(0);
|
|
26
|
-
}, 15_000);
|
|
27
|
-
it("searches issues", async () => {
|
|
28
|
-
const tool = github.tools.find((t) => t.name === "github_search_issues");
|
|
29
|
-
const result = await tool.handler({ query: "is:issue", repo: "octocat/Hello-World" }, ctx());
|
|
30
|
-
expect(result).toHaveProperty("items");
|
|
31
|
-
}, 15_000);
|
|
32
|
-
it("gets a file from a public repo", async () => {
|
|
33
|
-
const tool = github.tools.find((t) => t.name === "github_get_file");
|
|
34
|
-
const result = await tool.handler({ repo: "octocat/Hello-World", path: "README" }, ctx());
|
|
35
|
-
expect(result).toHaveProperty("content");
|
|
36
|
-
}, 15_000);
|
|
37
|
-
});
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Integration Test Harness
|
|
3
|
-
*
|
|
4
|
-
* Detects which external services are available via environment variables.
|
|
5
|
-
* Integration tests use `describe.runIf(available.xxx)` to skip cleanly
|
|
6
|
-
* when credentials aren't present.
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* npx vitest run --config vitest.integration.config.ts
|
|
10
|
-
*
|
|
11
|
-
* Or with env vars:
|
|
12
|
-
* GITHUB_TOKEN=ghp_... npx vitest run --config vitest.integration.config.ts
|
|
13
|
-
*/
|
|
14
|
-
export declare const available: {
|
|
15
|
-
github: boolean;
|
|
16
|
-
linear: boolean;
|
|
17
|
-
sentry: boolean;
|
|
18
|
-
datadog: boolean;
|
|
19
|
-
slack: boolean;
|
|
20
|
-
discord: boolean;
|
|
21
|
-
claude: boolean;
|
|
22
|
-
};
|
|
23
|
-
/** Print which services are available (for CI logs) */
|
|
24
|
-
export declare function logAvailability(): void;
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Integration Test Harness
|
|
3
|
-
*
|
|
4
|
-
* Detects which external services are available via environment variables.
|
|
5
|
-
* Integration tests use `describe.runIf(available.xxx)` to skip cleanly
|
|
6
|
-
* when credentials aren't present.
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* npx vitest run --config vitest.integration.config.ts
|
|
10
|
-
*
|
|
11
|
-
* Or with env vars:
|
|
12
|
-
* GITHUB_TOKEN=ghp_... npx vitest run --config vitest.integration.config.ts
|
|
13
|
-
*/
|
|
14
|
-
export const available = {
|
|
15
|
-
github: !!process.env.GITHUB_TOKEN,
|
|
16
|
-
linear: !!process.env.LINEAR_API_KEY,
|
|
17
|
-
sentry: !!(process.env.SENTRY_AUTH_TOKEN && process.env.SENTRY_ORG),
|
|
18
|
-
datadog: !!(process.env.DD_API_KEY && process.env.DD_APP_KEY),
|
|
19
|
-
slack: !!(process.env.SLACK_WEBHOOK_URL || process.env.SLACK_BOT_TOKEN),
|
|
20
|
-
discord: !!process.env.DISCORD_WEBHOOK_URL,
|
|
21
|
-
claude: !!process.env.ANTHROPIC_API_KEY,
|
|
22
|
-
};
|
|
23
|
-
/** Print which services are available (for CI logs) */
|
|
24
|
-
export function logAvailability() {
|
|
25
|
-
const entries = Object.entries(available);
|
|
26
|
-
const on = entries.filter(([, v]) => v).map(([k]) => k);
|
|
27
|
-
const off = entries.filter(([, v]) => !v).map(([k]) => k);
|
|
28
|
-
console.log(`\n🔌 Integration test services:`);
|
|
29
|
-
if (on.length > 0)
|
|
30
|
-
console.log(` ✅ Available: ${on.join(", ")}`);
|
|
31
|
-
if (off.length > 0)
|
|
32
|
-
console.log(` ⏭️ Skipped: ${off.join(", ")}`);
|
|
33
|
-
console.log();
|
|
34
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { available } from "./harness.js";
|
|
3
|
-
import { linear } from "../../skills/linear.js";
|
|
4
|
-
const ctx = () => ({
|
|
5
|
-
config: { LINEAR_API_KEY: process.env.LINEAR_API_KEY },
|
|
6
|
-
logger: console,
|
|
7
|
-
});
|
|
8
|
-
describe.runIf(available.linear)("linear integration", () => {
|
|
9
|
-
it("searches issues", async () => {
|
|
10
|
-
const tool = linear.tools.find((t) => t.name === "linear_search_issues");
|
|
11
|
-
const result = await tool.handler({ query: "bug", limit: 3 }, ctx());
|
|
12
|
-
expect(result).toHaveProperty("searchIssues");
|
|
13
|
-
expect(result.searchIssues).toHaveProperty("nodes");
|
|
14
|
-
}, 15_000);
|
|
15
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { available } from "./harness.js";
|
|
3
|
-
import { sentry } from "../../skills/sentry.js";
|
|
4
|
-
const ctx = () => ({
|
|
5
|
-
config: {
|
|
6
|
-
SENTRY_AUTH_TOKEN: process.env.SENTRY_AUTH_TOKEN,
|
|
7
|
-
SENTRY_ORG: process.env.SENTRY_ORG,
|
|
8
|
-
SENTRY_BASE_URL: process.env.SENTRY_BASE_URL ?? "",
|
|
9
|
-
},
|
|
10
|
-
logger: console,
|
|
11
|
-
});
|
|
12
|
-
describe.runIf(available.sentry)("sentry integration", () => {
|
|
13
|
-
// These tests require at least one project in the org
|
|
14
|
-
it("list_issues returns an array", async () => {
|
|
15
|
-
const tool = sentry.tools.find((t) => t.name === "sentry_list_issues");
|
|
16
|
-
// Use the first project found, or skip if none
|
|
17
|
-
const result = await tool.handler({ project: process.env.SENTRY_PROJECT ?? "javascript" }, ctx());
|
|
18
|
-
expect(Array.isArray(result)).toBe(true);
|
|
19
|
-
}, 15_000);
|
|
20
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { available } from "./harness.js";
|
|
3
|
-
import { slack } from "../../skills/slack.js";
|
|
4
|
-
const ctx = () => ({
|
|
5
|
-
config: {
|
|
6
|
-
SLACK_WEBHOOK_URL: process.env.SLACK_WEBHOOK_URL ?? "",
|
|
7
|
-
SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN ?? "",
|
|
8
|
-
},
|
|
9
|
-
logger: console,
|
|
10
|
-
});
|
|
11
|
-
// Only run Slack integration tests when explicitly opted in
|
|
12
|
-
// to avoid spamming channels during routine test runs
|
|
13
|
-
const slackEnabled = available.slack && process.env.SLACK_INTEGRATION_TEST === "1";
|
|
14
|
-
describe.runIf(slackEnabled)("slack integration", () => {
|
|
15
|
-
it("sends a test message via webhook", async () => {
|
|
16
|
-
if (!process.env.SLACK_WEBHOOK_URL)
|
|
17
|
-
return;
|
|
18
|
-
const tool = slack.tools.find((t) => t.name === "slack_send_message");
|
|
19
|
-
const result = await tool.handler({ text: `🧪 SWEny integration test — ${new Date().toISOString()}` }, ctx());
|
|
20
|
-
expect(result).toHaveProperty("ok");
|
|
21
|
-
}, 15_000);
|
|
22
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import { mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import * as path from "node:path";
|
|
5
|
-
import { randomBytes } from "node:crypto";
|
|
6
|
-
import { execute } from "../../executor.js";
|
|
7
|
-
import { MockClaude } from "../../testing.js";
|
|
8
|
-
import { createSkillMap } from "../../skills/index.js";
|
|
9
|
-
function freshDir(name) {
|
|
10
|
-
const dir = path.join(tmpdir(), `sweny-sources-int-${name}-${Date.now()}-${randomBytes(4).toString("hex")}`);
|
|
11
|
-
rmSync(dir, { recursive: true, force: true });
|
|
12
|
-
mkdirSync(dir, { recursive: true });
|
|
13
|
-
return dir;
|
|
14
|
-
}
|
|
15
|
-
describe("integration: Source resolution across a workflow", () => {
|
|
16
|
-
beforeEach(() => {
|
|
17
|
-
vi.restoreAllMocks();
|
|
18
|
-
});
|
|
19
|
-
it("resolves inline, file, and url instructions across three nodes", async () => {
|
|
20
|
-
const dir = freshDir("three-kinds");
|
|
21
|
-
writeFileSync(path.join(dir, "b.md"), "Step B instruction from file.");
|
|
22
|
-
vi.spyOn(globalThis, "fetch").mockImplementation(async () => new Response("Step C instruction from URL.", { status: 200 }));
|
|
23
|
-
const workflow = {
|
|
24
|
-
id: "three-kinds",
|
|
25
|
-
name: "Three Kinds",
|
|
26
|
-
description: "",
|
|
27
|
-
entry: "a",
|
|
28
|
-
nodes: {
|
|
29
|
-
a: { name: "A", instruction: "Step A inline", skills: [] },
|
|
30
|
-
b: { name: "B", instruction: { file: path.join(dir, "b.md") }, skills: [] },
|
|
31
|
-
c: { name: "C", instruction: { url: "https://example.com/c.md" }, skills: [] },
|
|
32
|
-
},
|
|
33
|
-
edges: [
|
|
34
|
-
{ from: "a", to: "b" },
|
|
35
|
-
{ from: "b", to: "c" },
|
|
36
|
-
],
|
|
37
|
-
};
|
|
38
|
-
const skills = createSkillMap([]);
|
|
39
|
-
const result = await execute(workflow, {}, {
|
|
40
|
-
skills,
|
|
41
|
-
claude: new MockClaude({
|
|
42
|
-
responses: {
|
|
43
|
-
a: { data: {} },
|
|
44
|
-
b: { data: {} },
|
|
45
|
-
c: { data: {} },
|
|
46
|
-
},
|
|
47
|
-
}),
|
|
48
|
-
cwd: dir,
|
|
49
|
-
});
|
|
50
|
-
// All three nodes should have executed
|
|
51
|
-
expect(result.results.size).toBe(3);
|
|
52
|
-
// Trace should have all sources
|
|
53
|
-
expect(result.trace.sources["nodes.a.instruction"].kind).toBe("inline");
|
|
54
|
-
expect(result.trace.sources["nodes.a.instruction"].content).toBe("Step A inline");
|
|
55
|
-
expect(result.trace.sources["nodes.b.instruction"].kind).toBe("file");
|
|
56
|
-
expect(result.trace.sources["nodes.b.instruction"].content).toBe("Step B instruction from file.");
|
|
57
|
-
expect(result.trace.sources["nodes.c.instruction"].kind).toBe("url");
|
|
58
|
-
expect(result.trace.sources["nodes.c.instruction"].content).toBe("Step C instruction from URL.");
|
|
59
|
-
expect(result.trace.sources["nodes.c.instruction"].fetchedAt).toBeDefined();
|
|
60
|
-
// Every source has a hash
|
|
61
|
-
for (const rs of Object.values(result.trace.sources)) {
|
|
62
|
-
expect(rs.hash).toMatch(/^[0-9a-f]{16}$/);
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
it("fails fast with SOURCE_OFFLINE_REQUIRES_FETCH when offline and URL present", async () => {
|
|
66
|
-
const workflow = {
|
|
67
|
-
id: "offline-fail",
|
|
68
|
-
name: "Offline fail",
|
|
69
|
-
description: "",
|
|
70
|
-
entry: "a",
|
|
71
|
-
nodes: {
|
|
72
|
-
a: { name: "A", instruction: { url: "https://example.com/x" }, skills: [] },
|
|
73
|
-
},
|
|
74
|
-
edges: [],
|
|
75
|
-
};
|
|
76
|
-
await expect(execute(workflow, {}, {
|
|
77
|
-
skills: createSkillMap([]),
|
|
78
|
-
claude: new MockClaude({ responses: { a: { data: {} } } }),
|
|
79
|
-
offline: true,
|
|
80
|
-
})).rejects.toThrow(/SOURCE_OFFLINE_REQUIRES_FETCH/);
|
|
81
|
-
});
|
|
82
|
-
it("fails fast with SOURCE_FILE_NOT_FOUND and includes field path", async () => {
|
|
83
|
-
const dir = freshDir("missing");
|
|
84
|
-
const workflow = {
|
|
85
|
-
id: "file-missing",
|
|
86
|
-
name: "File missing",
|
|
87
|
-
description: "",
|
|
88
|
-
entry: "bad",
|
|
89
|
-
nodes: {
|
|
90
|
-
bad: { name: "Bad", instruction: { file: "./does-not-exist.md" }, skills: [] },
|
|
91
|
-
},
|
|
92
|
-
edges: [],
|
|
93
|
-
};
|
|
94
|
-
await expect(execute(workflow, {}, {
|
|
95
|
-
skills: createSkillMap([]),
|
|
96
|
-
claude: new MockClaude({ responses: { bad: { data: {} } } }),
|
|
97
|
-
cwd: dir,
|
|
98
|
-
})).rejects.toThrow(/SOURCE_FILE_NOT_FOUND.*nodes\.bad\.instruction/);
|
|
99
|
-
});
|
|
100
|
-
it("emits workflow:start before sources:resolved (per spec)", async () => {
|
|
101
|
-
const events = [];
|
|
102
|
-
const workflow = {
|
|
103
|
-
id: "evt-test",
|
|
104
|
-
name: "Event test",
|
|
105
|
-
description: "",
|
|
106
|
-
entry: "only",
|
|
107
|
-
nodes: { only: { name: "Only", instruction: "Go.", skills: [] } },
|
|
108
|
-
edges: [],
|
|
109
|
-
};
|
|
110
|
-
await execute(workflow, {}, {
|
|
111
|
-
skills: createSkillMap([]),
|
|
112
|
-
claude: new MockClaude({ responses: { only: { data: {} } } }),
|
|
113
|
-
observer: (e) => events.push(e),
|
|
114
|
-
});
|
|
115
|
-
const srcEvt = events.find((e) => e.type === "sources:resolved");
|
|
116
|
-
expect(srcEvt).toBeDefined();
|
|
117
|
-
expect(srcEvt.sources["nodes.only.instruction"].content).toBe("Go.");
|
|
118
|
-
// workflow:start should come before sources:resolved (per spec)
|
|
119
|
-
const startIdx = events.findIndex((e) => e.type === "workflow:start");
|
|
120
|
-
const srcIdx = events.findIndex((e) => e.type === "sources:resolved");
|
|
121
|
-
expect(startIdx).toBeLessThan(srcIdx);
|
|
122
|
-
});
|
|
123
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
-
import * as fs from "node:fs";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import * as os from "node:os";
|
|
5
|
-
import { validateWorkflowFile, validateSkillDir } from "../cli/publish.js";
|
|
6
|
-
describe("validateWorkflowFile", () => {
|
|
7
|
-
let tmpDir;
|
|
8
|
-
beforeAll(() => {
|
|
9
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sweny-publish-test-"));
|
|
10
|
-
});
|
|
11
|
-
afterAll(() => {
|
|
12
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
13
|
-
});
|
|
14
|
-
it("validates a correct workflow file", () => {
|
|
15
|
-
const file = path.join(tmpDir, "good.yml");
|
|
16
|
-
fs.writeFileSync(file, `id: test-workflow
|
|
17
|
-
name: Test
|
|
18
|
-
description: A test workflow
|
|
19
|
-
entry: start
|
|
20
|
-
nodes:
|
|
21
|
-
start:
|
|
22
|
-
name: Start
|
|
23
|
-
instruction: Do something.
|
|
24
|
-
skills: [github]
|
|
25
|
-
edges: []
|
|
26
|
-
`);
|
|
27
|
-
const result = validateWorkflowFile(file);
|
|
28
|
-
expect(result.valid).toBe(true);
|
|
29
|
-
expect(result.id).toBe("test-workflow");
|
|
30
|
-
expect(result.name).toBe("Test");
|
|
31
|
-
expect(result.nodeCount).toBe(1);
|
|
32
|
-
expect(result.edgeCount).toBe(0);
|
|
33
|
-
expect(result.errors).toEqual([]);
|
|
34
|
-
});
|
|
35
|
-
it("rejects a non-existent file", () => {
|
|
36
|
-
const result = validateWorkflowFile(path.join(tmpDir, "nope.yml"));
|
|
37
|
-
expect(result.valid).toBe(false);
|
|
38
|
-
expect(result.errors[0]).toBe("File not found");
|
|
39
|
-
});
|
|
40
|
-
it("rejects invalid YAML", () => {
|
|
41
|
-
const file = path.join(tmpDir, "bad.yml");
|
|
42
|
-
fs.writeFileSync(file, "{{invalid yaml");
|
|
43
|
-
const result = validateWorkflowFile(file);
|
|
44
|
-
expect(result.valid).toBe(false);
|
|
45
|
-
expect(result.errors[0]).toMatch(/Invalid YAML/);
|
|
46
|
-
});
|
|
47
|
-
it("rejects a workflow missing required fields", () => {
|
|
48
|
-
const file = path.join(tmpDir, "incomplete.yml");
|
|
49
|
-
fs.writeFileSync(file, "id: test\nname: Test\n");
|
|
50
|
-
const result = validateWorkflowFile(file);
|
|
51
|
-
expect(result.valid).toBe(false);
|
|
52
|
-
expect(result.errors.length).toBeGreaterThan(0);
|
|
53
|
-
});
|
|
54
|
-
it("rejects a workflow with unknown skill references", () => {
|
|
55
|
-
const file = path.join(tmpDir, "unknown-skill.yml");
|
|
56
|
-
fs.writeFileSync(file, `id: unknown-skill-test
|
|
57
|
-
name: Unknown Skill Test
|
|
58
|
-
description: Test
|
|
59
|
-
entry: start
|
|
60
|
-
nodes:
|
|
61
|
-
start:
|
|
62
|
-
name: Start
|
|
63
|
-
instruction: Do something.
|
|
64
|
-
skills: [unicorn]
|
|
65
|
-
edges: []
|
|
66
|
-
`);
|
|
67
|
-
const result = validateWorkflowFile(file);
|
|
68
|
-
expect(result.valid).toBe(false);
|
|
69
|
-
expect(result.errors.some((e) => e.includes("unicorn"))).toBe(true);
|
|
70
|
-
});
|
|
71
|
-
it("returns config warnings for skills with required env vars", () => {
|
|
72
|
-
const file = path.join(tmpDir, "with-github.yml");
|
|
73
|
-
fs.writeFileSync(file, `id: github-test
|
|
74
|
-
name: GitHub Test
|
|
75
|
-
description: Test
|
|
76
|
-
entry: start
|
|
77
|
-
nodes:
|
|
78
|
-
start:
|
|
79
|
-
name: Start
|
|
80
|
-
instruction: Do something.
|
|
81
|
-
skills: [github]
|
|
82
|
-
edges: []
|
|
83
|
-
`);
|
|
84
|
-
const result = validateWorkflowFile(file);
|
|
85
|
-
expect(result.valid).toBe(true);
|
|
86
|
-
expect(result.warnings.some((w) => w.includes("GITHUB_TOKEN"))).toBe(true);
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
describe("validateSkillDir", () => {
|
|
90
|
-
let tmpDir;
|
|
91
|
-
beforeAll(() => {
|
|
92
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "sweny-skill-test-"));
|
|
93
|
-
});
|
|
94
|
-
afterAll(() => {
|
|
95
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
96
|
-
});
|
|
97
|
-
it("validates a correct instruction skill", () => {
|
|
98
|
-
const skillDir = path.join(tmpDir, "my-skill");
|
|
99
|
-
fs.mkdirSync(skillDir, { recursive: true });
|
|
100
|
-
fs.writeFileSync(path.join(skillDir, "SKILL.md"), `---
|
|
101
|
-
name: my-skill
|
|
102
|
-
description: A test skill
|
|
103
|
-
---
|
|
104
|
-
|
|
105
|
-
When reviewing code, check for security issues first.
|
|
106
|
-
`);
|
|
107
|
-
const result = validateSkillDir(skillDir);
|
|
108
|
-
expect(result.valid).toBe(true);
|
|
109
|
-
expect(result.id).toBe("my-skill");
|
|
110
|
-
expect(result.hasInstruction).toBe(true);
|
|
111
|
-
expect(result.hasMcp).toBe(false);
|
|
112
|
-
});
|
|
113
|
-
it("validates a correct MCP skill", () => {
|
|
114
|
-
const skillDir = path.join(tmpDir, "mcp-skill");
|
|
115
|
-
fs.mkdirSync(skillDir, { recursive: true });
|
|
116
|
-
fs.writeFileSync(path.join(skillDir, "SKILL.md"), `---
|
|
117
|
-
name: mcp-skill
|
|
118
|
-
description: A test MCP skill
|
|
119
|
-
mcp:
|
|
120
|
-
command: npx
|
|
121
|
-
args: ["-y", "@company/server"]
|
|
122
|
-
---
|
|
123
|
-
`);
|
|
124
|
-
const result = validateSkillDir(skillDir);
|
|
125
|
-
expect(result.valid).toBe(true);
|
|
126
|
-
expect(result.hasMcp).toBe(true);
|
|
127
|
-
});
|
|
128
|
-
it("rejects a directory without SKILL.md", () => {
|
|
129
|
-
const emptyDir = path.join(tmpDir, "empty");
|
|
130
|
-
fs.mkdirSync(emptyDir, { recursive: true });
|
|
131
|
-
const result = validateSkillDir(emptyDir);
|
|
132
|
-
expect(result.valid).toBe(false);
|
|
133
|
-
expect(result.errors[0]).toBe("SKILL.md not found in directory");
|
|
134
|
-
});
|
|
135
|
-
it("rejects a skill with no frontmatter", () => {
|
|
136
|
-
const skillDir = path.join(tmpDir, "no-fm");
|
|
137
|
-
fs.mkdirSync(skillDir, { recursive: true });
|
|
138
|
-
fs.writeFileSync(path.join(skillDir, "SKILL.md"), "Just some text");
|
|
139
|
-
const result = validateSkillDir(skillDir);
|
|
140
|
-
expect(result.valid).toBe(false);
|
|
141
|
-
expect(result.errors[0]).toBe("No YAML frontmatter found");
|
|
142
|
-
});
|
|
143
|
-
it("rejects an invalid skill ID", () => {
|
|
144
|
-
const skillDir = path.join(tmpDir, "Bad-Name");
|
|
145
|
-
fs.mkdirSync(skillDir, { recursive: true });
|
|
146
|
-
fs.writeFileSync(path.join(skillDir, "SKILL.md"), `---
|
|
147
|
-
name: Bad-Name
|
|
148
|
-
---
|
|
149
|
-
|
|
150
|
-
Some instruction.
|
|
151
|
-
`);
|
|
152
|
-
const result = validateSkillDir(skillDir);
|
|
153
|
-
expect(result.valid).toBe(false);
|
|
154
|
-
expect(result.errors[0]).toMatch(/Invalid skill ID/);
|
|
155
|
-
});
|
|
156
|
-
it("rejects a skill with mismatched directory name", () => {
|
|
157
|
-
const skillDir = path.join(tmpDir, "wrong-dir");
|
|
158
|
-
fs.mkdirSync(skillDir, { recursive: true });
|
|
159
|
-
fs.writeFileSync(path.join(skillDir, "SKILL.md"), `---
|
|
160
|
-
name: correct-name
|
|
161
|
-
---
|
|
162
|
-
|
|
163
|
-
Some instruction.
|
|
164
|
-
`);
|
|
165
|
-
const result = validateSkillDir(skillDir);
|
|
166
|
-
expect(result.valid).toBe(false);
|
|
167
|
-
expect(result.errors).toContain(`Directory name "wrong-dir" doesn't match skill name "correct-name"`);
|
|
168
|
-
});
|
|
169
|
-
it("rejects a skill with no instruction and no MCP", () => {
|
|
170
|
-
const skillDir = path.join(tmpDir, "empty-skill");
|
|
171
|
-
fs.mkdirSync(skillDir, { recursive: true });
|
|
172
|
-
fs.writeFileSync(path.join(skillDir, "SKILL.md"), `---
|
|
173
|
-
name: empty-skill
|
|
174
|
-
---
|
|
175
|
-
`);
|
|
176
|
-
const result = validateSkillDir(skillDir);
|
|
177
|
-
expect(result.valid).toBe(false);
|
|
178
|
-
expect(result.errors).toContain("Skill must have an instruction body or an mcp config");
|
|
179
|
-
});
|
|
180
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|