@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
package/dist/mermaid.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/mermaid.test.js
DELETED
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { toMermaid, toMermaidBlock } from "./mermaid.js";
|
|
3
|
-
import { triageWorkflow, implementWorkflow } from "./workflows/index.js";
|
|
4
|
-
// ─── Fixtures ──────────────────────────────────────────────────────
|
|
5
|
-
const simple = {
|
|
6
|
-
id: "simple",
|
|
7
|
-
name: "Simple",
|
|
8
|
-
description: "A→B→C",
|
|
9
|
-
entry: "a",
|
|
10
|
-
nodes: {
|
|
11
|
-
a: { name: "Triage", instruction: "Triage the alert", skills: ["github"] },
|
|
12
|
-
b: { name: "Investigate", instruction: "Investigate the issue", skills: ["sentry"] },
|
|
13
|
-
c: { name: "Report", instruction: "Write report", skills: ["slack"] },
|
|
14
|
-
},
|
|
15
|
-
edges: [
|
|
16
|
-
{ from: "a", to: "b" },
|
|
17
|
-
{ from: "b", to: "c" },
|
|
18
|
-
],
|
|
19
|
-
};
|
|
20
|
-
const branching = {
|
|
21
|
-
id: "branching",
|
|
22
|
-
name: "Branching",
|
|
23
|
-
description: "Conditional routing",
|
|
24
|
-
entry: "triage",
|
|
25
|
-
nodes: {
|
|
26
|
-
triage: { name: "Triage", instruction: "Classify severity", skills: [] },
|
|
27
|
-
investigate: { name: "Investigate", instruction: "Deep dive", skills: [] },
|
|
28
|
-
notify: { name: "Notify", instruction: "Send alert", skills: [] },
|
|
29
|
-
close: { name: "Close", instruction: "Close ticket", skills: [] },
|
|
30
|
-
},
|
|
31
|
-
edges: [
|
|
32
|
-
{ from: "triage", to: "investigate", when: "severity is high or critical" },
|
|
33
|
-
{ from: "triage", to: "notify", when: "severity is low" },
|
|
34
|
-
{ from: "investigate", to: "close" },
|
|
35
|
-
{ from: "notify", to: "close" },
|
|
36
|
-
],
|
|
37
|
-
};
|
|
38
|
-
const withLoop = {
|
|
39
|
-
id: "loop",
|
|
40
|
-
name: "Loop",
|
|
41
|
-
description: "Retry loop",
|
|
42
|
-
entry: "a",
|
|
43
|
-
nodes: {
|
|
44
|
-
a: { name: "Try", instruction: "Attempt fix", skills: [] },
|
|
45
|
-
b: { name: "Done", instruction: "Wrap up", skills: [] },
|
|
46
|
-
},
|
|
47
|
-
edges: [
|
|
48
|
-
{ from: "a", to: "a", when: "needs retry", max_iterations: 3 },
|
|
49
|
-
{ from: "a", to: "b" },
|
|
50
|
-
],
|
|
51
|
-
};
|
|
52
|
-
// ─── Tests ─────────────────────────────────────────────────────────
|
|
53
|
-
describe("toMermaid", () => {
|
|
54
|
-
it("renders a simple linear workflow", () => {
|
|
55
|
-
const result = toMermaid(simple);
|
|
56
|
-
expect(result).toContain("graph TB");
|
|
57
|
-
// Entry node gets stadium shape
|
|
58
|
-
expect(result).toContain("a([Triage \u25B6])");
|
|
59
|
-
// Non-entry nodes get default shape
|
|
60
|
-
expect(result).toContain("b[Investigate]");
|
|
61
|
-
expect(result).toContain("c[Report]");
|
|
62
|
-
// Edges
|
|
63
|
-
expect(result).toContain("a --> b");
|
|
64
|
-
expect(result).toContain("b --> c");
|
|
65
|
-
});
|
|
66
|
-
it("renders conditional edges with labels", () => {
|
|
67
|
-
const result = toMermaid(branching);
|
|
68
|
-
expect(result).toContain('triage -->|"severity is high or critical"| investigate');
|
|
69
|
-
expect(result).toContain('triage -->|"severity is low"| notify');
|
|
70
|
-
expect(result).toContain("investigate --> close");
|
|
71
|
-
expect(result).toContain("notify --> close");
|
|
72
|
-
});
|
|
73
|
-
it("renders max_iterations on edges", () => {
|
|
74
|
-
const result = toMermaid(withLoop);
|
|
75
|
-
expect(result).toContain('a -->|"needs retry · max 3x"| a');
|
|
76
|
-
expect(result).toContain("a --> b");
|
|
77
|
-
});
|
|
78
|
-
it("renders max_iterations without condition", () => {
|
|
79
|
-
const wf = {
|
|
80
|
-
...simple,
|
|
81
|
-
edges: [
|
|
82
|
-
{ from: "a", to: "b", max_iterations: 5 },
|
|
83
|
-
{ from: "b", to: "c" },
|
|
84
|
-
],
|
|
85
|
-
};
|
|
86
|
-
const result = toMermaid(wf);
|
|
87
|
-
expect(result).toContain('a -->|"max 5x"| b');
|
|
88
|
-
});
|
|
89
|
-
it("applies execution state styling", () => {
|
|
90
|
-
const result = toMermaid(simple, {
|
|
91
|
-
state: {
|
|
92
|
-
a: "success",
|
|
93
|
-
b: "current",
|
|
94
|
-
},
|
|
95
|
-
});
|
|
96
|
-
expect(result).toContain("classDef current fill:#3b82f6");
|
|
97
|
-
expect(result).toContain("classDef success fill:#22c55e");
|
|
98
|
-
expect(result).toContain("class a success");
|
|
99
|
-
expect(result).toContain("class b current");
|
|
100
|
-
// c has no state — no class applied
|
|
101
|
-
expect(result).not.toContain("class c");
|
|
102
|
-
});
|
|
103
|
-
it("applies failed and skipped styling", () => {
|
|
104
|
-
const result = toMermaid(simple, {
|
|
105
|
-
state: {
|
|
106
|
-
a: "success",
|
|
107
|
-
b: "failed",
|
|
108
|
-
c: "skipped",
|
|
109
|
-
},
|
|
110
|
-
});
|
|
111
|
-
expect(result).toContain("classDef failed fill:#ef4444");
|
|
112
|
-
expect(result).toContain("classDef skipped fill:#6b7280");
|
|
113
|
-
expect(result).toContain("class b failed");
|
|
114
|
-
expect(result).toContain("class c skipped");
|
|
115
|
-
});
|
|
116
|
-
it("omits style block when no state provided", () => {
|
|
117
|
-
const result = toMermaid(simple);
|
|
118
|
-
expect(result).not.toContain("classDef");
|
|
119
|
-
expect(result).not.toContain("class ");
|
|
120
|
-
});
|
|
121
|
-
it("supports LR direction", () => {
|
|
122
|
-
const result = toMermaid(simple, { direction: "LR" });
|
|
123
|
-
expect(result).toContain("graph LR");
|
|
124
|
-
});
|
|
125
|
-
it("supports title", () => {
|
|
126
|
-
const result = toMermaid(simple, { title: "My Workflow" });
|
|
127
|
-
expect(result).toContain("---");
|
|
128
|
-
expect(result).toContain("title: My Workflow");
|
|
129
|
-
});
|
|
130
|
-
it("sanitizes node IDs with special characters", () => {
|
|
131
|
-
const wf = {
|
|
132
|
-
id: "special",
|
|
133
|
-
name: "Special",
|
|
134
|
-
description: "",
|
|
135
|
-
entry: "step.one",
|
|
136
|
-
nodes: {
|
|
137
|
-
"step.one": { name: "Step One", instruction: "Do it", skills: [] },
|
|
138
|
-
"step two": { name: "Step Two", instruction: "Do it", skills: [] },
|
|
139
|
-
},
|
|
140
|
-
edges: [{ from: "step.one", to: "step two" }],
|
|
141
|
-
};
|
|
142
|
-
const result = toMermaid(wf);
|
|
143
|
-
expect(result).toContain("step_one");
|
|
144
|
-
expect(result).toContain("step_two");
|
|
145
|
-
expect(result).not.toContain("step.one");
|
|
146
|
-
expect(result).not.toContain("step two");
|
|
147
|
-
});
|
|
148
|
-
it("escapes special characters in labels", () => {
|
|
149
|
-
const wf = {
|
|
150
|
-
id: "escape",
|
|
151
|
-
name: "Escape",
|
|
152
|
-
description: "",
|
|
153
|
-
entry: "a",
|
|
154
|
-
nodes: {
|
|
155
|
-
a: { name: 'Say "hello"', instruction: "x", skills: [] },
|
|
156
|
-
b: { name: "Done", instruction: "x", skills: [] },
|
|
157
|
-
},
|
|
158
|
-
edges: [{ from: "a", to: "b" }],
|
|
159
|
-
};
|
|
160
|
-
const result = toMermaid(wf);
|
|
161
|
-
expect(result).toContain(""");
|
|
162
|
-
expect(result).not.toContain('Say "hello"');
|
|
163
|
-
});
|
|
164
|
-
it("groups multiple nodes with same status", () => {
|
|
165
|
-
const result = toMermaid(simple, {
|
|
166
|
-
state: { a: "success", b: "success", c: "current" },
|
|
167
|
-
});
|
|
168
|
-
expect(result).toContain("class a,b success");
|
|
169
|
-
expect(result).toContain("class c current");
|
|
170
|
-
});
|
|
171
|
-
it("highlights taken edges and dims not-taken edges with trace", () => {
|
|
172
|
-
const trace = {
|
|
173
|
-
steps: [
|
|
174
|
-
{ node: "triage", status: "success", iteration: 1 },
|
|
175
|
-
{ node: "investigate", status: "success", iteration: 1 },
|
|
176
|
-
{ node: "close", status: "success", iteration: 1 },
|
|
177
|
-
],
|
|
178
|
-
edges: [
|
|
179
|
-
{ from: "triage", to: "investigate", reason: "severity is high or critical" },
|
|
180
|
-
{ from: "investigate", to: "close", reason: "only path" },
|
|
181
|
-
],
|
|
182
|
-
sources: {},
|
|
183
|
-
};
|
|
184
|
-
const result = toMermaid(branching, { trace });
|
|
185
|
-
// Taken edges get green bold styling (edges 0 and 2 in the workflow)
|
|
186
|
-
expect(result).toContain("linkStyle");
|
|
187
|
-
expect(result).toContain("stroke:#22c55e,stroke-width:3px");
|
|
188
|
-
// Not-taken edges get dashed gray
|
|
189
|
-
expect(result).toContain("stroke:#6b7280,stroke-width:1px,stroke-dasharray:5 5");
|
|
190
|
-
});
|
|
191
|
-
it("shows iteration count on nodes and edges from trace", () => {
|
|
192
|
-
const trace = {
|
|
193
|
-
steps: [
|
|
194
|
-
{ node: "a", status: "success", iteration: 1 },
|
|
195
|
-
{ node: "a", status: "success", iteration: 2 },
|
|
196
|
-
{ node: "b", status: "success", iteration: 1 },
|
|
197
|
-
],
|
|
198
|
-
edges: [
|
|
199
|
-
{ from: "a", to: "a", reason: "needs retry" },
|
|
200
|
-
{ from: "a", to: "b", reason: "only path" },
|
|
201
|
-
],
|
|
202
|
-
sources: {},
|
|
203
|
-
};
|
|
204
|
-
const result = toMermaid(withLoop, { trace });
|
|
205
|
-
// Node a ran twice — should show ×2
|
|
206
|
-
expect(result).toContain("×2");
|
|
207
|
-
// Node b ran once — no iteration tag
|
|
208
|
-
expect(result).not.toContain("Done ×");
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
describe("builtin workflows", () => {
|
|
212
|
-
it("renders triage workflow", () => {
|
|
213
|
-
const result = toMermaid(triageWorkflow);
|
|
214
|
-
expect(result).toContain("graph TB");
|
|
215
|
-
// Every node should appear
|
|
216
|
-
for (const [id, node] of Object.entries(triageWorkflow.nodes)) {
|
|
217
|
-
expect(result).toContain(node.name);
|
|
218
|
-
}
|
|
219
|
-
// Every edge should appear
|
|
220
|
-
for (const edge of triageWorkflow.edges) {
|
|
221
|
-
expect(result).toContain(edge.from);
|
|
222
|
-
expect(result).toContain(edge.to);
|
|
223
|
-
}
|
|
224
|
-
});
|
|
225
|
-
it("renders implement workflow", () => {
|
|
226
|
-
const result = toMermaid(implementWorkflow);
|
|
227
|
-
expect(result).toContain("graph TB");
|
|
228
|
-
for (const node of Object.values(implementWorkflow.nodes)) {
|
|
229
|
-
expect(result).toContain(node.name);
|
|
230
|
-
}
|
|
231
|
-
});
|
|
232
|
-
it("renders triage with execution state", () => {
|
|
233
|
-
const ids = Object.keys(triageWorkflow.nodes);
|
|
234
|
-
const state = {};
|
|
235
|
-
state[ids[0]] = "success";
|
|
236
|
-
if (ids[1])
|
|
237
|
-
state[ids[1]] = "current";
|
|
238
|
-
const result = toMermaid(triageWorkflow, { state, title: triageWorkflow.name });
|
|
239
|
-
expect(result).toContain("classDef success");
|
|
240
|
-
expect(result).toContain("classDef current");
|
|
241
|
-
expect(result).toContain(`class ${ids[0]} success`);
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
describe("toMermaidBlock", () => {
|
|
245
|
-
it("wraps in fenced code block", () => {
|
|
246
|
-
const result = toMermaidBlock(simple);
|
|
247
|
-
expect(result).toMatch(/^```mermaid\n/);
|
|
248
|
-
expect(result).toMatch(/\n```$/);
|
|
249
|
-
expect(result).toContain("graph TB");
|
|
250
|
-
});
|
|
251
|
-
it("passes options through", () => {
|
|
252
|
-
const result = toMermaidBlock(simple, { direction: "LR", state: { a: "success" } });
|
|
253
|
-
expect(result).toContain("graph LR");
|
|
254
|
-
expect(result).toContain("class a success");
|
|
255
|
-
});
|
|
256
|
-
});
|
package/dist/sources.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/sources.test.js
DELETED
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
5
|
-
import { classifySource, sourceZ } from "./sources.js";
|
|
6
|
-
import { hashContent, resolveSource, resolveSources } from "./source-resolver.js";
|
|
7
|
-
const fileTmp = path.join(tmpdir(), "sweny-sources-file-test");
|
|
8
|
-
const baseCtx = () => ({
|
|
9
|
-
cwd: "/tmp",
|
|
10
|
-
env: {},
|
|
11
|
-
authConfig: {},
|
|
12
|
-
offline: false,
|
|
13
|
-
logger: { info: () => { }, warn: () => { }, error: () => { }, debug: () => { } },
|
|
14
|
-
});
|
|
15
|
-
describe("classifySource", () => {
|
|
16
|
-
it("classifies http(s) URLs as url", () => {
|
|
17
|
-
expect(classifySource("http://example.com/x.md")).toBe("url");
|
|
18
|
-
expect(classifySource("https://example.com/x.md")).toBe("url");
|
|
19
|
-
});
|
|
20
|
-
it("classifies ./ ../ and / paths as file", () => {
|
|
21
|
-
expect(classifySource("./local.md")).toBe("file");
|
|
22
|
-
expect(classifySource("../sibling.md")).toBe("file");
|
|
23
|
-
expect(classifySource("/abs/path.md")).toBe("file");
|
|
24
|
-
});
|
|
25
|
-
it("classifies anything else as inline", () => {
|
|
26
|
-
expect(classifySource("Just be helpful.")).toBe("inline");
|
|
27
|
-
expect(classifySource("foo bar baz")).toBe("inline");
|
|
28
|
-
});
|
|
29
|
-
it("rejects empty strings", () => {
|
|
30
|
-
expect(() => classifySource("")).toThrow(/empty/i);
|
|
31
|
-
expect(() => classifySource(" ")).toThrow(/empty/i);
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
describe("sourceZ", () => {
|
|
35
|
-
it("accepts non-empty string", () => {
|
|
36
|
-
expect(sourceZ.parse("hello")).toBe("hello");
|
|
37
|
-
expect(sourceZ.parse("./x.md")).toBe("./x.md");
|
|
38
|
-
expect(sourceZ.parse("https://x/y")).toBe("https://x/y");
|
|
39
|
-
});
|
|
40
|
-
it("rejects empty string", () => {
|
|
41
|
-
expect(() => sourceZ.parse("")).toThrow();
|
|
42
|
-
});
|
|
43
|
-
it("accepts tagged inline form", () => {
|
|
44
|
-
expect(sourceZ.parse({ inline: "text" })).toEqual({ inline: "text" });
|
|
45
|
-
});
|
|
46
|
-
it("accepts tagged file form", () => {
|
|
47
|
-
expect(sourceZ.parse({ file: "./x.md" })).toEqual({ file: "./x.md" });
|
|
48
|
-
});
|
|
49
|
-
it("accepts tagged url form with and without type", () => {
|
|
50
|
-
expect(sourceZ.parse({ url: "https://x" })).toEqual({ url: "https://x" });
|
|
51
|
-
expect(sourceZ.parse({ url: "https://x", type: "fetch" })).toEqual({
|
|
52
|
-
url: "https://x",
|
|
53
|
-
type: "fetch",
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
it("rejects objects with multiple tag keys", () => {
|
|
57
|
-
expect(() => sourceZ.parse({ inline: "x", file: "./y" })).toThrow();
|
|
58
|
-
expect(() => sourceZ.parse({ file: "./x", url: "https://y" })).toThrow();
|
|
59
|
-
});
|
|
60
|
-
it("rejects objects with no tag keys", () => {
|
|
61
|
-
expect(() => sourceZ.parse({})).toThrow();
|
|
62
|
-
expect(() => sourceZ.parse({ foo: "bar" })).toThrow();
|
|
63
|
-
});
|
|
64
|
-
it("rejects extra keys on tagged forms", () => {
|
|
65
|
-
expect(() => sourceZ.parse({ inline: "x", extra: true })).toThrow();
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
describe("hashContent", () => {
|
|
69
|
-
it("produces stable 16-char hex for same content", () => {
|
|
70
|
-
const a = hashContent("hello world");
|
|
71
|
-
const b = hashContent("hello world");
|
|
72
|
-
expect(a).toBe(b);
|
|
73
|
-
expect(a).toMatch(/^[0-9a-f]{16}$/);
|
|
74
|
-
});
|
|
75
|
-
it("produces different hashes for different content", () => {
|
|
76
|
-
expect(hashContent("a")).not.toBe(hashContent("b"));
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
describe("resolveSource (inline)", () => {
|
|
80
|
-
it("returns the plain string for inline-classified strings", async () => {
|
|
81
|
-
const resolved = await resolveSource("Just do it.", "test.field", baseCtx());
|
|
82
|
-
expect(resolved.content).toBe("Just do it.");
|
|
83
|
-
expect(resolved.kind).toBe("inline");
|
|
84
|
-
expect(resolved.resolver).toBe("inline");
|
|
85
|
-
expect(resolved.origin).toBe("Just do it.");
|
|
86
|
-
expect(resolved.hash).toMatch(/^[0-9a-f]{16}$/);
|
|
87
|
-
expect(resolved.fetchedAt).toBeUndefined();
|
|
88
|
-
expect(resolved.sourcePath).toBeUndefined();
|
|
89
|
-
});
|
|
90
|
-
it("returns the inline text from tagged {inline} form", async () => {
|
|
91
|
-
const resolved = await resolveSource({ inline: "Tagged body" }, "test.field", baseCtx());
|
|
92
|
-
expect(resolved.content).toBe("Tagged body");
|
|
93
|
-
expect(resolved.kind).toBe("inline");
|
|
94
|
-
expect(resolved.resolver).toBe("inline");
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
describe("resolveSource (file)", () => {
|
|
98
|
-
beforeEach(() => {
|
|
99
|
-
rmSync(fileTmp, { recursive: true, force: true });
|
|
100
|
-
mkdirSync(fileTmp, { recursive: true });
|
|
101
|
-
});
|
|
102
|
-
it("reads a file by relative path (cwd-based)", async () => {
|
|
103
|
-
writeFileSync(path.join(fileTmp, "rules.md"), "# Rules\n\nBe kind.");
|
|
104
|
-
const resolved = await resolveSource("./rules.md", "rules[0]", {
|
|
105
|
-
...baseCtx(),
|
|
106
|
-
cwd: fileTmp,
|
|
107
|
-
});
|
|
108
|
-
expect(resolved.content).toBe("# Rules\n\nBe kind.");
|
|
109
|
-
expect(resolved.kind).toBe("file");
|
|
110
|
-
expect(resolved.resolver).toBe("file");
|
|
111
|
-
expect(resolved.sourcePath).toBe(path.resolve(fileTmp, "rules.md"));
|
|
112
|
-
});
|
|
113
|
-
it("reads a file via tagged {file} form", async () => {
|
|
114
|
-
writeFileSync(path.join(fileTmp, "a.txt"), "alpha");
|
|
115
|
-
const resolved = await resolveSource({ file: "./a.txt" }, "x", {
|
|
116
|
-
...baseCtx(),
|
|
117
|
-
cwd: fileTmp,
|
|
118
|
-
});
|
|
119
|
-
expect(resolved.content).toBe("alpha");
|
|
120
|
-
expect(resolved.origin).toEqual({ file: "./a.txt" });
|
|
121
|
-
});
|
|
122
|
-
it("reads an absolute path", async () => {
|
|
123
|
-
const abs = path.join(fileTmp, "abs.md");
|
|
124
|
-
writeFileSync(abs, "absolute body");
|
|
125
|
-
const resolved = await resolveSource(abs, "x", baseCtx());
|
|
126
|
-
expect(resolved.content).toBe("absolute body");
|
|
127
|
-
});
|
|
128
|
-
it("throws SOURCE_FILE_NOT_FOUND with field path on missing file", async () => {
|
|
129
|
-
await expect(resolveSource("./missing.md", "nodes.investigate.instruction", {
|
|
130
|
-
...baseCtx(),
|
|
131
|
-
cwd: fileTmp,
|
|
132
|
-
})).rejects.toThrow(/SOURCE_FILE_NOT_FOUND.*nodes\.investigate\.instruction/);
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
describe("resolveSource (fetch)", () => {
|
|
136
|
-
beforeEach(() => {
|
|
137
|
-
vi.restoreAllMocks();
|
|
138
|
-
});
|
|
139
|
-
it("fetches URL content via plain string", async () => {
|
|
140
|
-
const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(async () => {
|
|
141
|
-
return new Response("# Shared rules\n\nBe careful.", {
|
|
142
|
-
status: 200,
|
|
143
|
-
headers: { "content-type": "text/markdown" },
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
const resolved = await resolveSource("https://example.com/rules.md", "rules[0]", baseCtx());
|
|
147
|
-
expect(resolved.content).toBe("# Shared rules\n\nBe careful.");
|
|
148
|
-
expect(resolved.kind).toBe("url");
|
|
149
|
-
expect(resolved.resolver).toBe("fetch");
|
|
150
|
-
expect(resolved.fetchedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
151
|
-
expect(fetchMock).toHaveBeenCalledOnce();
|
|
152
|
-
});
|
|
153
|
-
it("fetches via tagged {url} form", async () => {
|
|
154
|
-
vi.spyOn(globalThis, "fetch").mockImplementation(async () => new Response("body", { status: 200 }));
|
|
155
|
-
const resolved = await resolveSource({ url: "https://x.test/y", type: "fetch" }, "x", baseCtx());
|
|
156
|
-
expect(resolved.content).toBe("body");
|
|
157
|
-
});
|
|
158
|
-
it("throws SOURCE_URL_HTTP_ERROR on 4xx", async () => {
|
|
159
|
-
vi.spyOn(globalThis, "fetch").mockImplementation(async () => new Response("nope", { status: 404 }));
|
|
160
|
-
await expect(resolveSource("https://x.test/missing", "nodes.n.instruction", baseCtx())).rejects.toThrow(/SOURCE_URL_HTTP_ERROR.*404.*nodes\.n\.instruction/);
|
|
161
|
-
});
|
|
162
|
-
it("throws SOURCE_URL_AUTH_REQUIRED on 401", async () => {
|
|
163
|
-
vi.spyOn(globalThis, "fetch").mockImplementation(async () => new Response("denied", { status: 401 }));
|
|
164
|
-
await expect(resolveSource("https://x.test/auth", "x", baseCtx())).rejects.toThrow(/SOURCE_URL_AUTH_REQUIRED/);
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
describe("fetch auth headers", () => {
|
|
168
|
-
beforeEach(() => {
|
|
169
|
-
vi.restoreAllMocks();
|
|
170
|
-
});
|
|
171
|
-
it("sets Authorization from per-host fetch.auth config", async () => {
|
|
172
|
-
const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(async () => new Response("ok", { status: 200 }));
|
|
173
|
-
await resolveSource("https://raw.githubusercontent.com/x/y/main/z.md", "f", {
|
|
174
|
-
...baseCtx(),
|
|
175
|
-
env: { GITHUB_TOKEN: "ghp_abc" },
|
|
176
|
-
authConfig: { "raw.githubusercontent.com": "GITHUB_TOKEN" },
|
|
177
|
-
});
|
|
178
|
-
const [, init] = fetchMock.mock.calls[0];
|
|
179
|
-
expect(init.headers).toMatchObject({
|
|
180
|
-
Authorization: "Bearer ghp_abc",
|
|
181
|
-
});
|
|
182
|
-
});
|
|
183
|
-
it("falls back to SWENY_FETCH_TOKEN when no per-host match", async () => {
|
|
184
|
-
const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(async () => new Response("ok", { status: 200 }));
|
|
185
|
-
await resolveSource("https://other.example/x", "f", {
|
|
186
|
-
...baseCtx(),
|
|
187
|
-
env: { SWENY_FETCH_TOKEN: "global_token" },
|
|
188
|
-
authConfig: { "raw.githubusercontent.com": "GITHUB_TOKEN" },
|
|
189
|
-
});
|
|
190
|
-
const [, init] = fetchMock.mock.calls[0];
|
|
191
|
-
expect(init.headers).toMatchObject({
|
|
192
|
-
Authorization: "Bearer global_token",
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
it("omits Authorization when env var is unset", async () => {
|
|
196
|
-
const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(async () => new Response("ok", { status: 200 }));
|
|
197
|
-
await resolveSource("https://raw.githubusercontent.com/x", "f", {
|
|
198
|
-
...baseCtx(),
|
|
199
|
-
env: {},
|
|
200
|
-
authConfig: { "raw.githubusercontent.com": "GITHUB_TOKEN" },
|
|
201
|
-
});
|
|
202
|
-
const [, init] = fetchMock.mock.calls[0];
|
|
203
|
-
const headers = init.headers;
|
|
204
|
-
expect(headers.Authorization).toBeUndefined();
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
describe("offline mode", () => {
|
|
208
|
-
beforeEach(() => {
|
|
209
|
-
vi.restoreAllMocks();
|
|
210
|
-
rmSync(fileTmp, { recursive: true, force: true });
|
|
211
|
-
mkdirSync(fileTmp, { recursive: true });
|
|
212
|
-
});
|
|
213
|
-
it("file resolution still works when offline", async () => {
|
|
214
|
-
writeFileSync(path.join(fileTmp, "local.md"), "hi");
|
|
215
|
-
const resolved = await resolveSource("./local.md", "f", {
|
|
216
|
-
...baseCtx(),
|
|
217
|
-
cwd: fileTmp,
|
|
218
|
-
offline: true,
|
|
219
|
-
});
|
|
220
|
-
expect(resolved.content).toBe("hi");
|
|
221
|
-
});
|
|
222
|
-
it("url resolution fails with SOURCE_OFFLINE_REQUIRES_FETCH when offline", async () => {
|
|
223
|
-
await expect(resolveSource("https://example.com/x", "rules[0]", {
|
|
224
|
-
...baseCtx(),
|
|
225
|
-
offline: true,
|
|
226
|
-
})).rejects.toThrow(/SOURCE_OFFLINE_REQUIRES_FETCH.*https:\/\/example\.com\/x.*rules\[0\]/);
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
describe("resolveSources (orchestrator)", () => {
|
|
230
|
-
beforeEach(() => {
|
|
231
|
-
vi.restoreAllMocks();
|
|
232
|
-
rmSync(fileTmp, { recursive: true, force: true });
|
|
233
|
-
mkdirSync(fileTmp, { recursive: true });
|
|
234
|
-
});
|
|
235
|
-
it("resolves a map of Sources and returns a parallel ResolvedSource map", async () => {
|
|
236
|
-
writeFileSync(path.join(fileTmp, "a.md"), "alpha");
|
|
237
|
-
const fetchMock = vi
|
|
238
|
-
.spyOn(globalThis, "fetch")
|
|
239
|
-
.mockImplementation(async () => new Response("beta", { status: 200 }));
|
|
240
|
-
const out = await resolveSources({
|
|
241
|
-
"nodes.n.instruction": "Inline thing.",
|
|
242
|
-
"rules[0]": "./a.md",
|
|
243
|
-
"context[0]": "https://x.test/doc",
|
|
244
|
-
}, { ...baseCtx(), cwd: fileTmp });
|
|
245
|
-
expect(out["nodes.n.instruction"].content).toBe("Inline thing.");
|
|
246
|
-
expect(out["rules[0]"].content).toBe("alpha");
|
|
247
|
-
expect(out["context[0]"].content).toBe("beta");
|
|
248
|
-
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
249
|
-
});
|
|
250
|
-
it("caches by canonical key: same URL referenced twice fetches once", async () => {
|
|
251
|
-
const fetchMock = vi
|
|
252
|
-
.spyOn(globalThis, "fetch")
|
|
253
|
-
.mockImplementation(async () => new Response("shared", { status: 200 }));
|
|
254
|
-
const out = await resolveSources({
|
|
255
|
-
"rules[0]": "https://x.test/shared.md",
|
|
256
|
-
"context[0]": "https://x.test/shared.md",
|
|
257
|
-
}, baseCtx());
|
|
258
|
-
expect(out["rules[0]"].content).toBe("shared");
|
|
259
|
-
expect(out["context[0]"].content).toBe("shared");
|
|
260
|
-
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
261
|
-
});
|
|
262
|
-
it("reports the first failure's field path", async () => {
|
|
263
|
-
await expect(resolveSources({ "nodes.bad.instruction": "./does-not-exist.md" }, { ...baseCtx(), cwd: fileTmp })).rejects.toThrow(/SOURCE_FILE_NOT_FOUND.*nodes\.bad\.instruction/);
|
|
264
|
-
});
|
|
265
|
-
});
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Comprehensive validation tests for the unified Source type.
|
|
3
|
-
*
|
|
4
|
-
* These tests verify correctness, completeness, and best practices
|
|
5
|
-
* beyond the unit tests in sources.test.ts. They cover:
|
|
6
|
-
* - Review fix features: retry on 5xx, BOM stripping, SOURCE_INVALID_TYPE, tagged trimming
|
|
7
|
-
* - Edge cases: whitespace, unicode, large content, concurrent resolution
|
|
8
|
-
* - Schema validation: sourceZ boundary cases
|
|
9
|
-
* - Error message quality: field paths, remediation hints
|
|
10
|
-
* - Security: no auth token leakage, URL validation
|
|
11
|
-
*/
|
|
12
|
-
export {};
|