@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,317 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import { compareVersions, detectPackageManager, resolveInstallRoot, installCommandFor, runUpgrade, fetchLatestFromNpm, } from "../upgrade.js";
|
|
3
|
-
// In-memory writable stream for pure assertions on output. Cheaper and
|
|
4
|
-
// simpler than a real Node stream and identical to the helper used by
|
|
5
|
-
// diagram.test.ts — keeps test utilities consistent across the cli/ dir.
|
|
6
|
-
function captureStream() {
|
|
7
|
-
const chunks = [];
|
|
8
|
-
const stream = {
|
|
9
|
-
write: (chunk) => {
|
|
10
|
-
chunks.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"));
|
|
11
|
-
return true;
|
|
12
|
-
},
|
|
13
|
-
};
|
|
14
|
-
return {
|
|
15
|
-
stream,
|
|
16
|
-
get text() {
|
|
17
|
-
return chunks.join("");
|
|
18
|
-
},
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Build an UpgradeDeps object with sensible defaults. Tests override only
|
|
23
|
-
* the bits relevant to the behavior they're asserting — keeps each case
|
|
24
|
-
* focused on its condition rather than plumbing.
|
|
25
|
-
*/
|
|
26
|
-
function makeDeps(over = {}) {
|
|
27
|
-
const stdout = captureStream();
|
|
28
|
-
const stderr = captureStream();
|
|
29
|
-
const exit = vi.fn();
|
|
30
|
-
const runInstall = vi.fn(() => 0);
|
|
31
|
-
const deps = {
|
|
32
|
-
currentVersion: "0.1.65",
|
|
33
|
-
installPath: "/Users/x/.nvm/versions/node/v22/lib/node_modules/@sweny-ai/core/dist/cli/main.js",
|
|
34
|
-
fetchLatestVersion: vi.fn(async () => "0.1.66"),
|
|
35
|
-
runInstall,
|
|
36
|
-
canWrite: () => true,
|
|
37
|
-
stdout: stdout.stream,
|
|
38
|
-
stderr: stderr.stream,
|
|
39
|
-
exit,
|
|
40
|
-
...over,
|
|
41
|
-
};
|
|
42
|
-
return { deps, stdout, stderr, exit, runInstall };
|
|
43
|
-
}
|
|
44
|
-
describe("compareVersions", () => {
|
|
45
|
-
it("ranks by numeric core segments", () => {
|
|
46
|
-
expect(compareVersions("0.1.10", "0.1.9")).toBeGreaterThan(0);
|
|
47
|
-
expect(compareVersions("0.1.9", "0.1.10")).toBeLessThan(0);
|
|
48
|
-
expect(compareVersions("1.0.0", "0.9.999")).toBeGreaterThan(0);
|
|
49
|
-
expect(compareVersions("0.1.65", "0.1.65")).toBe(0);
|
|
50
|
-
});
|
|
51
|
-
it("handles missing segments as zero", () => {
|
|
52
|
-
expect(compareVersions("1", "1.0.0")).toBe(0);
|
|
53
|
-
expect(compareVersions("1.0", "1.0.1")).toBeLessThan(0);
|
|
54
|
-
});
|
|
55
|
-
it("ranks pre-releases below their release", () => {
|
|
56
|
-
expect(compareVersions("1.0.0-alpha.1", "1.0.0")).toBeLessThan(0);
|
|
57
|
-
expect(compareVersions("1.0.0", "1.0.0-beta.3")).toBeGreaterThan(0);
|
|
58
|
-
expect(compareVersions("1.0.0-alpha", "1.0.0-beta")).toBeLessThan(0);
|
|
59
|
-
});
|
|
60
|
-
it("treats non-numeric core segments as zero (defensive)", () => {
|
|
61
|
-
// Malformed numeric segments shouldn't throw or return NaN — they
|
|
62
|
-
// collapse to 0 so the comparison still returns a deterministic ordering.
|
|
63
|
-
expect(compareVersions("1.x.0", "1.0.0")).toBe(0);
|
|
64
|
-
expect(compareVersions("1.0.y", "1.0.0")).toBe(0);
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
describe("detectPackageManager", () => {
|
|
68
|
-
it("detects npm from nvm-style layout", () => {
|
|
69
|
-
expect(detectPackageManager("/Users/x/.nvm/versions/node/v22.22.0/lib/node_modules/@sweny-ai/core/dist/cli/main.js")).toBe("npm");
|
|
70
|
-
});
|
|
71
|
-
it("detects npm from fnm / asdf / vanilla Node paths", () => {
|
|
72
|
-
expect(detectPackageManager("/usr/lib/node_modules/@sweny-ai/core/dist/cli/main.js")).toBe("npm");
|
|
73
|
-
expect(detectPackageManager("/home/x/.fnm/node-versions/v22/installation/lib/node_modules/@sweny-ai/core/dist/cli/main.js")).toBe("npm");
|
|
74
|
-
});
|
|
75
|
-
it("detects pnpm global installs", () => {
|
|
76
|
-
expect(detectPackageManager("/Users/x/.local/share/pnpm/global/5/node_modules/@sweny-ai/core/dist/cli/main.js")).toBe("pnpm");
|
|
77
|
-
expect(detectPackageManager("/Users/x/Library/pnpm-global/5/node_modules/@sweny-ai/core/dist/cli/main.js")).toBe("pnpm");
|
|
78
|
-
});
|
|
79
|
-
it("detects homebrew", () => {
|
|
80
|
-
expect(detectPackageManager("/opt/homebrew/Cellar/sweny/0.1.65/libexec/bin/sweny")).toBe("brew");
|
|
81
|
-
expect(detectPackageManager("/usr/local/Cellar/sweny/0.1.65/libexec/bin/sweny")).toBe("brew");
|
|
82
|
-
});
|
|
83
|
-
it("detects volta-managed installs", () => {
|
|
84
|
-
expect(detectPackageManager("/Users/x/.volta/tools/image/packages/@sweny-ai/core/bin/sweny")).toBe("volta");
|
|
85
|
-
});
|
|
86
|
-
it("detects bun global installs", () => {
|
|
87
|
-
expect(detectPackageManager("/Users/x/.bun/install/global/node_modules/@sweny-ai/core/dist/cli/main.js")).toBe("bun");
|
|
88
|
-
});
|
|
89
|
-
it("detects yarn global", () => {
|
|
90
|
-
expect(detectPackageManager("/Users/x/.yarn/global/node_modules/@sweny-ai/core/dist/cli/main.js")).toBe("yarn");
|
|
91
|
-
});
|
|
92
|
-
it("normalizes Windows paths (forward-slash rules)", () => {
|
|
93
|
-
const win = "C:\\Users\\x\\AppData\\Roaming\\npm\\node_modules\\@sweny-ai\\core\\dist\\cli\\main.js";
|
|
94
|
-
expect(detectPackageManager(win)).toBe("npm");
|
|
95
|
-
});
|
|
96
|
-
it("returns 'unknown' for paths that don't look like a package-manager install", () => {
|
|
97
|
-
expect(detectPackageManager("/tmp/some-random-path/main.js")).toBe("unknown");
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
describe("resolveInstallRoot", () => {
|
|
101
|
-
it("returns the prefix above node_modules for a typical npm layout", () => {
|
|
102
|
-
const p = "/Users/x/.nvm/versions/node/v22/lib/node_modules/@sweny-ai/core/dist/cli/main.js";
|
|
103
|
-
expect(resolveInstallRoot(p)).toBe("/Users/x/.nvm/versions/node/v22/lib");
|
|
104
|
-
});
|
|
105
|
-
it("returns null when @sweny-ai is not in the parent chain", () => {
|
|
106
|
-
expect(resolveInstallRoot("/tmp/random/main.js")).toBeNull();
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
describe("installCommandFor", () => {
|
|
110
|
-
it("emits the correct CLI invocation per package manager", () => {
|
|
111
|
-
expect(installCommandFor("npm", "latest")).toEqual({
|
|
112
|
-
cmd: "npm",
|
|
113
|
-
args: ["install", "-g", "@sweny-ai/core@latest"],
|
|
114
|
-
});
|
|
115
|
-
expect(installCommandFor("pnpm", "latest")).toEqual({ cmd: "pnpm", args: ["add", "-g", "@sweny-ai/core@latest"] });
|
|
116
|
-
expect(installCommandFor("yarn", "latest")).toEqual({
|
|
117
|
-
cmd: "yarn",
|
|
118
|
-
args: ["global", "add", "@sweny-ai/core@latest"],
|
|
119
|
-
});
|
|
120
|
-
expect(installCommandFor("bun", "latest")).toEqual({ cmd: "bun", args: ["add", "-g", "@sweny-ai/core@latest"] });
|
|
121
|
-
expect(installCommandFor("volta", "latest")).toEqual({ cmd: "volta", args: ["install", "@sweny-ai/core@latest"] });
|
|
122
|
-
});
|
|
123
|
-
it("passes through non-default tags", () => {
|
|
124
|
-
expect(installCommandFor("npm", "beta")).toEqual({ cmd: "npm", args: ["install", "-g", "@sweny-ai/core@beta"] });
|
|
125
|
-
});
|
|
126
|
-
it("returns null for brew / unknown (can't drive automatically)", () => {
|
|
127
|
-
expect(installCommandFor("brew", "latest")).toBeNull();
|
|
128
|
-
expect(installCommandFor("unknown", "latest")).toBeNull();
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
describe("runUpgrade — no-op paths", () => {
|
|
132
|
-
it("reports already-latest and exits 0", async () => {
|
|
133
|
-
const { deps, stdout, exit, runInstall } = makeDeps({
|
|
134
|
-
currentVersion: "0.1.66",
|
|
135
|
-
fetchLatestVersion: vi.fn(async () => "0.1.66"),
|
|
136
|
-
});
|
|
137
|
-
await runUpgrade({}, deps);
|
|
138
|
-
expect(stdout.text).toMatch(/already the latest/);
|
|
139
|
-
expect(runInstall).not.toHaveBeenCalled();
|
|
140
|
-
expect(exit).toHaveBeenCalledWith(0);
|
|
141
|
-
});
|
|
142
|
-
it("reports when running ahead of the registry (local build) and exits 0", async () => {
|
|
143
|
-
const { deps, stdout, exit, runInstall } = makeDeps({
|
|
144
|
-
currentVersion: "0.2.0",
|
|
145
|
-
fetchLatestVersion: vi.fn(async () => "0.1.66"),
|
|
146
|
-
});
|
|
147
|
-
await runUpgrade({}, deps);
|
|
148
|
-
expect(stdout.text).toMatch(/newer than the published/);
|
|
149
|
-
expect(runInstall).not.toHaveBeenCalled();
|
|
150
|
-
expect(exit).toHaveBeenCalledWith(0);
|
|
151
|
-
});
|
|
152
|
-
it("installs anyway when --force is set even if already latest", async () => {
|
|
153
|
-
const { deps, runInstall, exit } = makeDeps({
|
|
154
|
-
currentVersion: "0.1.66",
|
|
155
|
-
fetchLatestVersion: vi.fn(async () => "0.1.66"),
|
|
156
|
-
});
|
|
157
|
-
await runUpgrade({ force: true }, deps);
|
|
158
|
-
expect(runInstall).toHaveBeenCalledWith("npm", ["install", "-g", "@sweny-ai/core@latest"]);
|
|
159
|
-
expect(exit).toHaveBeenCalledWith(0);
|
|
160
|
-
});
|
|
161
|
-
it("downgrades when --force is set against a local-dev build ahead of the registry", async () => {
|
|
162
|
-
const { deps, runInstall, exit } = makeDeps({
|
|
163
|
-
currentVersion: "0.2.0",
|
|
164
|
-
fetchLatestVersion: vi.fn(async () => "0.1.66"),
|
|
165
|
-
});
|
|
166
|
-
await runUpgrade({ force: true }, deps);
|
|
167
|
-
// --force explicitly overrides the "ahead" short-circuit — this is how
|
|
168
|
-
// someone pins back to the published train from a stale local build.
|
|
169
|
-
expect(runInstall).toHaveBeenCalledWith("npm", ["install", "-g", "@sweny-ai/core@latest"]);
|
|
170
|
-
expect(exit).toHaveBeenCalledWith(0);
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
describe("runUpgrade — install dispatch", () => {
|
|
174
|
-
it("invokes the detected package manager and exits 0 on success", async () => {
|
|
175
|
-
const { deps, stdout, runInstall, exit } = makeDeps();
|
|
176
|
-
await runUpgrade({}, deps);
|
|
177
|
-
expect(runInstall).toHaveBeenCalledWith("npm", ["install", "-g", "@sweny-ai/core@latest"]);
|
|
178
|
-
expect(stdout.text).toMatch(/Upgrading sweny 0\.1\.65 → 0\.1\.66/);
|
|
179
|
-
expect(stdout.text).toMatch(/✓ Upgraded sweny to 0\.1\.66/);
|
|
180
|
-
expect(exit).toHaveBeenCalledWith(0);
|
|
181
|
-
});
|
|
182
|
-
it("uses pnpm when the install path is a pnpm global root", async () => {
|
|
183
|
-
const { deps, runInstall } = makeDeps({
|
|
184
|
-
installPath: "/Users/x/.local/share/pnpm/global/5/node_modules/@sweny-ai/core/dist/cli/main.js",
|
|
185
|
-
});
|
|
186
|
-
await runUpgrade({}, deps);
|
|
187
|
-
expect(runInstall).toHaveBeenCalledWith("pnpm", ["add", "-g", "@sweny-ai/core@latest"]);
|
|
188
|
-
});
|
|
189
|
-
it("forwards a non-default --tag to the install command", async () => {
|
|
190
|
-
const { deps, runInstall } = makeDeps({
|
|
191
|
-
fetchLatestVersion: vi.fn(async (tag) => (tag === "beta" ? "0.2.0-beta.1" : "0.1.66")),
|
|
192
|
-
});
|
|
193
|
-
await runUpgrade({ tag: "beta" }, deps);
|
|
194
|
-
expect(runInstall).toHaveBeenCalledWith("npm", ["install", "-g", "@sweny-ai/core@beta"]);
|
|
195
|
-
});
|
|
196
|
-
it("propagates a non-zero install exit code", async () => {
|
|
197
|
-
const { deps, stderr, exit } = makeDeps({
|
|
198
|
-
runInstall: vi.fn(() => 42),
|
|
199
|
-
});
|
|
200
|
-
await runUpgrade({}, deps);
|
|
201
|
-
expect(stderr.text).toMatch(/exited with code 42/);
|
|
202
|
-
expect(exit).toHaveBeenCalledWith(42);
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
describe("runUpgrade — permissions + platform edges", () => {
|
|
206
|
-
it("prints sudo hint and exits 1 when the install prefix isn't writable", async () => {
|
|
207
|
-
const { deps, stderr, exit, runInstall } = makeDeps({
|
|
208
|
-
canWrite: () => false,
|
|
209
|
-
});
|
|
210
|
-
await runUpgrade({}, deps);
|
|
211
|
-
expect(stderr.text).toMatch(/no write permission/);
|
|
212
|
-
expect(stderr.text).toMatch(/sudo npm install -g @sweny-ai\/core@latest/);
|
|
213
|
-
expect(runInstall).not.toHaveBeenCalled();
|
|
214
|
-
expect(exit).toHaveBeenCalledWith(1);
|
|
215
|
-
});
|
|
216
|
-
it("defers to brew for homebrew installs (doesn't try to npm-install over it)", async () => {
|
|
217
|
-
const { deps, stdout, stderr, exit, runInstall } = makeDeps({
|
|
218
|
-
installPath: "/opt/homebrew/Cellar/sweny/0.1.65/libexec/bin/sweny",
|
|
219
|
-
});
|
|
220
|
-
await runUpgrade({}, deps);
|
|
221
|
-
expect(stdout.text).toMatch(/0\.1\.66 is available/);
|
|
222
|
-
expect(stderr.text).toMatch(/brew upgrade sweny/);
|
|
223
|
-
expect(runInstall).not.toHaveBeenCalled();
|
|
224
|
-
expect(exit).toHaveBeenCalledWith(1);
|
|
225
|
-
});
|
|
226
|
-
it("prints a manual-install hint for unknown layouts", async () => {
|
|
227
|
-
const { deps, stdout, stderr, exit, runInstall } = makeDeps({
|
|
228
|
-
installPath: "/tmp/some-random-path/main.js",
|
|
229
|
-
});
|
|
230
|
-
await runUpgrade({}, deps);
|
|
231
|
-
expect(stdout.text).toMatch(/0\.1\.66 is available/);
|
|
232
|
-
expect(stderr.text).toMatch(/npm install -g @sweny-ai\/core@latest/);
|
|
233
|
-
expect(runInstall).not.toHaveBeenCalled();
|
|
234
|
-
expect(exit).toHaveBeenCalledWith(1);
|
|
235
|
-
});
|
|
236
|
-
});
|
|
237
|
-
describe("runUpgrade — --check (dry run)", () => {
|
|
238
|
-
it("reports the command without running it", async () => {
|
|
239
|
-
const { deps, stdout, runInstall, exit } = makeDeps();
|
|
240
|
-
await runUpgrade({ check: true }, deps);
|
|
241
|
-
expect(stdout.text).toMatch(/0\.1\.66 is available/);
|
|
242
|
-
expect(stdout.text).toMatch(/npm install -g @sweny-ai\/core@latest/);
|
|
243
|
-
expect(runInstall).not.toHaveBeenCalled();
|
|
244
|
-
expect(exit).toHaveBeenCalledWith(0);
|
|
245
|
-
});
|
|
246
|
-
it("still short-circuits on already-latest under --check", async () => {
|
|
247
|
-
const { deps, stdout, exit } = makeDeps({
|
|
248
|
-
currentVersion: "0.1.66",
|
|
249
|
-
fetchLatestVersion: vi.fn(async () => "0.1.66"),
|
|
250
|
-
});
|
|
251
|
-
await runUpgrade({ check: true }, deps);
|
|
252
|
-
expect(stdout.text).toMatch(/already the latest/);
|
|
253
|
-
expect(exit).toHaveBeenCalledWith(0);
|
|
254
|
-
});
|
|
255
|
-
});
|
|
256
|
-
describe("runUpgrade — error handling", () => {
|
|
257
|
-
it("exits 1 with a helpful message when the registry fetch fails", async () => {
|
|
258
|
-
const { deps, stderr, exit, runInstall } = makeDeps({
|
|
259
|
-
fetchLatestVersion: vi.fn(async () => {
|
|
260
|
-
throw new Error("ENETDOWN");
|
|
261
|
-
}),
|
|
262
|
-
});
|
|
263
|
-
await runUpgrade({}, deps);
|
|
264
|
-
expect(stderr.text).toMatch(/couldn't reach the npm registry/);
|
|
265
|
-
expect(stderr.text).toMatch(/ENETDOWN/);
|
|
266
|
-
expect(runInstall).not.toHaveBeenCalled();
|
|
267
|
-
expect(exit).toHaveBeenCalledWith(1);
|
|
268
|
-
});
|
|
269
|
-
});
|
|
270
|
-
describe("fetchLatestFromNpm", () => {
|
|
271
|
-
it("parses the dist-tag JSON and returns the version", async () => {
|
|
272
|
-
const fetchSpy = vi
|
|
273
|
-
.spyOn(globalThis, "fetch")
|
|
274
|
-
.mockImplementation(async () => new Response(JSON.stringify({ version: "9.9.9" }), { status: 200 }));
|
|
275
|
-
try {
|
|
276
|
-
expect(await fetchLatestFromNpm("latest")).toBe("9.9.9");
|
|
277
|
-
expect(fetchSpy).toHaveBeenCalledWith("https://registry.npmjs.org/@sweny-ai/core/latest", expect.objectContaining({ headers: expect.objectContaining({ accept: "application/json" }) }));
|
|
278
|
-
}
|
|
279
|
-
finally {
|
|
280
|
-
fetchSpy.mockRestore();
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
it("throws a clear error when the registry returns non-2xx", async () => {
|
|
284
|
-
const fetchSpy = vi
|
|
285
|
-
.spyOn(globalThis, "fetch")
|
|
286
|
-
.mockImplementation(async () => new Response("not found", { status: 404, statusText: "Not Found" }));
|
|
287
|
-
try {
|
|
288
|
-
await expect(fetchLatestFromNpm("bogus-tag")).rejects.toThrow(/404/);
|
|
289
|
-
}
|
|
290
|
-
finally {
|
|
291
|
-
fetchSpy.mockRestore();
|
|
292
|
-
}
|
|
293
|
-
});
|
|
294
|
-
it("throws when the registry payload is missing `version`", async () => {
|
|
295
|
-
const fetchSpy = vi
|
|
296
|
-
.spyOn(globalThis, "fetch")
|
|
297
|
-
.mockImplementation(async () => new Response(JSON.stringify({}), { status: 200 }));
|
|
298
|
-
try {
|
|
299
|
-
await expect(fetchLatestFromNpm("latest")).rejects.toThrow(/version/);
|
|
300
|
-
}
|
|
301
|
-
finally {
|
|
302
|
-
fetchSpy.mockRestore();
|
|
303
|
-
}
|
|
304
|
-
});
|
|
305
|
-
it("URL-encodes the tag so custom tags don't break the path", async () => {
|
|
306
|
-
const fetchSpy = vi
|
|
307
|
-
.spyOn(globalThis, "fetch")
|
|
308
|
-
.mockImplementation(async () => new Response(JSON.stringify({ version: "1.0.0" }), { status: 200 }));
|
|
309
|
-
try {
|
|
310
|
-
await fetchLatestFromNpm("next beta");
|
|
311
|
-
expect(fetchSpy).toHaveBeenCalledWith("https://registry.npmjs.org/@sweny-ai/core/next%20beta", expect.anything());
|
|
312
|
-
}
|
|
313
|
-
finally {
|
|
314
|
-
fetchSpy.mockRestore();
|
|
315
|
-
}
|
|
316
|
-
});
|
|
317
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import * as fs from "node:fs";
|
|
3
|
-
import * as os from "node:os";
|
|
4
|
-
import * as path from "node:path";
|
|
5
|
-
import { shouldSkip, readCache, writeCache, isCacheFresh, formatNudge, defaultCachePath, maybeNudge, CACHE_TTL_MS, } from "../version-check.js";
|
|
6
|
-
function captureStream() {
|
|
7
|
-
const chunks = [];
|
|
8
|
-
const stream = {
|
|
9
|
-
write: (chunk) => {
|
|
10
|
-
chunks.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"));
|
|
11
|
-
return true;
|
|
12
|
-
},
|
|
13
|
-
};
|
|
14
|
-
return {
|
|
15
|
-
stream,
|
|
16
|
-
get text() {
|
|
17
|
-
return chunks.join("");
|
|
18
|
-
},
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
describe("shouldSkip", () => {
|
|
22
|
-
it("skips in CI", () => {
|
|
23
|
-
expect(shouldSkip({ CI: "1" }, true, "run")).toBe(true);
|
|
24
|
-
});
|
|
25
|
-
it("skips when SWENY_NO_UPDATE_CHECK is set", () => {
|
|
26
|
-
expect(shouldSkip({ SWENY_NO_UPDATE_CHECK: "1" }, true, "run")).toBe(true);
|
|
27
|
-
});
|
|
28
|
-
it("skips when SWENY_OFFLINE is set", () => {
|
|
29
|
-
expect(shouldSkip({ SWENY_OFFLINE: "1" }, true, "run")).toBe(true);
|
|
30
|
-
});
|
|
31
|
-
it("skips non-TTY invocations (pipes, scripts)", () => {
|
|
32
|
-
expect(shouldSkip({}, false, "run")).toBe(true);
|
|
33
|
-
});
|
|
34
|
-
it("skips the upgrade command itself to avoid double-printing", () => {
|
|
35
|
-
expect(shouldSkip({}, true, "upgrade")).toBe(true);
|
|
36
|
-
expect(shouldSkip({}, true, "update")).toBe(true);
|
|
37
|
-
});
|
|
38
|
-
it("allows the nudge in an interactive TTY with no opt-out", () => {
|
|
39
|
-
expect(shouldSkip({}, true, "run")).toBe(false);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
describe("cache I/O", () => {
|
|
43
|
-
let tmpdir;
|
|
44
|
-
let cachePath;
|
|
45
|
-
beforeEach(() => {
|
|
46
|
-
tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "sweny-version-check-"));
|
|
47
|
-
cachePath = path.join(tmpdir, "sub", "version-check.json");
|
|
48
|
-
});
|
|
49
|
-
afterEach(() => {
|
|
50
|
-
fs.rmSync(tmpdir, { recursive: true, force: true });
|
|
51
|
-
});
|
|
52
|
-
it("round-trips a valid cache entry", () => {
|
|
53
|
-
writeCache(cachePath, { latest: "0.1.66", checkedAt: 12345 });
|
|
54
|
-
expect(readCache(cachePath)).toEqual({ latest: "0.1.66", checkedAt: 12345 });
|
|
55
|
-
});
|
|
56
|
-
it("creates parent directories as needed", () => {
|
|
57
|
-
writeCache(cachePath, { latest: "0.1.66", checkedAt: 12345 });
|
|
58
|
-
expect(fs.existsSync(cachePath)).toBe(true);
|
|
59
|
-
});
|
|
60
|
-
it("returns null for a missing cache file", () => {
|
|
61
|
-
expect(readCache(cachePath)).toBeNull();
|
|
62
|
-
});
|
|
63
|
-
it("returns null when the cache is malformed JSON", () => {
|
|
64
|
-
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
65
|
-
fs.writeFileSync(cachePath, "{{{not json");
|
|
66
|
-
expect(readCache(cachePath)).toBeNull();
|
|
67
|
-
});
|
|
68
|
-
it("returns null when required fields are missing or wrong type", () => {
|
|
69
|
-
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
70
|
-
fs.writeFileSync(cachePath, JSON.stringify({ latest: "0.1.66" }));
|
|
71
|
-
expect(readCache(cachePath)).toBeNull();
|
|
72
|
-
fs.writeFileSync(cachePath, JSON.stringify({ checkedAt: 1 }));
|
|
73
|
-
expect(readCache(cachePath)).toBeNull();
|
|
74
|
-
fs.writeFileSync(cachePath, JSON.stringify({ latest: "", checkedAt: 1 }));
|
|
75
|
-
expect(readCache(cachePath)).toBeNull();
|
|
76
|
-
});
|
|
77
|
-
it("never throws on unwritable cache directories (degrades silently)", () => {
|
|
78
|
-
// Point at a path where the parent is a file — mkdirSync will throw,
|
|
79
|
-
// and writeCache must swallow it.
|
|
80
|
-
const blocker = path.join(tmpdir, "blocker");
|
|
81
|
-
fs.writeFileSync(blocker, "im-a-file");
|
|
82
|
-
expect(() => writeCache(path.join(blocker, "cache.json"), { latest: "0.1.66", checkedAt: 1 })).not.toThrow();
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
describe("isCacheFresh", () => {
|
|
86
|
-
it("returns true within TTL", () => {
|
|
87
|
-
expect(isCacheFresh({ latest: "x", checkedAt: 1000 }, 1000 + CACHE_TTL_MS - 1)).toBe(true);
|
|
88
|
-
});
|
|
89
|
-
it("returns false at or past TTL", () => {
|
|
90
|
-
expect(isCacheFresh({ latest: "x", checkedAt: 1000 }, 1000 + CACHE_TTL_MS)).toBe(false);
|
|
91
|
-
expect(isCacheFresh({ latest: "x", checkedAt: 1000 }, 1000 + CACHE_TTL_MS + 1)).toBe(false);
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
describe("formatNudge", () => {
|
|
95
|
-
it("includes both versions and the upgrade command", () => {
|
|
96
|
-
const out = formatNudge("0.1.65", "0.1.66");
|
|
97
|
-
expect(out).toContain("0.1.65");
|
|
98
|
-
expect(out).toContain("0.1.66");
|
|
99
|
-
expect(out).toContain("sweny upgrade");
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
describe("defaultCachePath", () => {
|
|
103
|
-
it("honors XDG_CACHE_HOME when set", () => {
|
|
104
|
-
const p = defaultCachePath({ XDG_CACHE_HOME: "/custom/cache" }, "/home/x");
|
|
105
|
-
expect(p).toBe(path.join("/custom/cache", "sweny", "version-check.json"));
|
|
106
|
-
});
|
|
107
|
-
it("falls back to ~/.cache when XDG_CACHE_HOME is unset", () => {
|
|
108
|
-
expect(defaultCachePath({}, "/home/x")).toBe(path.join("/home/x/.cache", "sweny", "version-check.json"));
|
|
109
|
-
});
|
|
110
|
-
it("treats empty XDG_CACHE_HOME as unset (defensive)", () => {
|
|
111
|
-
expect(defaultCachePath({ XDG_CACHE_HOME: "" }, "/home/x")).toBe(path.join("/home/x/.cache", "sweny", "version-check.json"));
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
describe("maybeNudge", () => {
|
|
115
|
-
let tmpdir;
|
|
116
|
-
let cachePath;
|
|
117
|
-
let stderr;
|
|
118
|
-
const fetchOk = vi.fn(async () => "9.9.9");
|
|
119
|
-
beforeEach(() => {
|
|
120
|
-
tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "sweny-nudge-"));
|
|
121
|
-
cachePath = path.join(tmpdir, "version-check.json");
|
|
122
|
-
stderr = captureStream();
|
|
123
|
-
fetchOk.mockClear();
|
|
124
|
-
});
|
|
125
|
-
afterEach(() => {
|
|
126
|
-
fs.rmSync(tmpdir, { recursive: true, force: true });
|
|
127
|
-
});
|
|
128
|
-
function baseDeps(over = {}) {
|
|
129
|
-
return {
|
|
130
|
-
currentVersion: "0.1.65",
|
|
131
|
-
cachePath,
|
|
132
|
-
now: 1_000_000,
|
|
133
|
-
env: {},
|
|
134
|
-
isTty: true,
|
|
135
|
-
commandName: "run",
|
|
136
|
-
stderr: stderr.stream,
|
|
137
|
-
fetchLatestVersion: fetchOk,
|
|
138
|
-
...over,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
it("prints nudge when cache shows a newer release", async () => {
|
|
142
|
-
writeCache(cachePath, { latest: "0.1.66", checkedAt: 1_000_000 });
|
|
143
|
-
await maybeNudge(baseDeps());
|
|
144
|
-
expect(stderr.text).toMatch(/0\.1\.66 is available/);
|
|
145
|
-
expect(stderr.text).toMatch(/sweny upgrade/);
|
|
146
|
-
});
|
|
147
|
-
it("stays silent when cache is equal or lower than current", async () => {
|
|
148
|
-
writeCache(cachePath, { latest: "0.1.65", checkedAt: 1_000_000 });
|
|
149
|
-
await maybeNudge(baseDeps());
|
|
150
|
-
expect(stderr.text).toBe("");
|
|
151
|
-
});
|
|
152
|
-
it("refreshes the cache when stale and writes the fetched version", async () => {
|
|
153
|
-
writeCache(cachePath, { latest: "0.1.64", checkedAt: 0 });
|
|
154
|
-
await maybeNudge(baseDeps({ now: CACHE_TTL_MS + 10 }));
|
|
155
|
-
expect(fetchOk).toHaveBeenCalledWith("latest");
|
|
156
|
-
const after = readCache(cachePath);
|
|
157
|
-
expect(after?.latest).toBe("9.9.9");
|
|
158
|
-
expect(after?.checkedAt).toBe(CACHE_TTL_MS + 10);
|
|
159
|
-
});
|
|
160
|
-
it("refreshes when cache is missing and does not crash when fetch fails", async () => {
|
|
161
|
-
const fetchFail = vi.fn(async () => {
|
|
162
|
-
throw new Error("ENETDOWN");
|
|
163
|
-
});
|
|
164
|
-
await maybeNudge(baseDeps({ fetchLatestVersion: fetchFail }));
|
|
165
|
-
expect(fetchFail).toHaveBeenCalled();
|
|
166
|
-
expect(readCache(cachePath)).toBeNull(); // fetch failed, nothing written
|
|
167
|
-
expect(stderr.text).toBe(""); // silent on error
|
|
168
|
-
});
|
|
169
|
-
it("gives up silently when the registry is slower than the budget", async () => {
|
|
170
|
-
// The race between fetch and the internal timer means a slow fetch should
|
|
171
|
-
// resolve to null → no cache write → no nudge. We pin down the behavior
|
|
172
|
-
// here so a future regression (forgetting the race or the timeout) gets
|
|
173
|
-
// caught even though the budget is configured in the module itself.
|
|
174
|
-
const slowFetch = vi.fn(() => new Promise((resolve) => {
|
|
175
|
-
// Never resolves within the test — the internal timer is what wins.
|
|
176
|
-
const t = setTimeout(() => resolve("9.9.9"), 10_000);
|
|
177
|
-
if (typeof t.unref === "function") {
|
|
178
|
-
t.unref();
|
|
179
|
-
}
|
|
180
|
-
}));
|
|
181
|
-
await maybeNudge(baseDeps({ fetchLatestVersion: slowFetch }));
|
|
182
|
-
expect(slowFetch).toHaveBeenCalled();
|
|
183
|
-
expect(readCache(cachePath)).toBeNull();
|
|
184
|
-
expect(stderr.text).toBe("");
|
|
185
|
-
});
|
|
186
|
-
it("does not refresh when cache is fresh (saves bandwidth)", async () => {
|
|
187
|
-
writeCache(cachePath, { latest: "0.1.65", checkedAt: 1_000_000 });
|
|
188
|
-
await maybeNudge(baseDeps({ now: 1_000_000 + 1 }));
|
|
189
|
-
expect(fetchOk).not.toHaveBeenCalled();
|
|
190
|
-
});
|
|
191
|
-
it("skips entirely in CI — no nudge, no fetch", async () => {
|
|
192
|
-
writeCache(cachePath, { latest: "0.1.66", checkedAt: 1_000_000 });
|
|
193
|
-
await maybeNudge(baseDeps({ env: { CI: "1" } }));
|
|
194
|
-
expect(stderr.text).toBe("");
|
|
195
|
-
expect(fetchOk).not.toHaveBeenCalled();
|
|
196
|
-
});
|
|
197
|
-
it("skips entirely on non-TTY stderr", async () => {
|
|
198
|
-
writeCache(cachePath, { latest: "0.1.66", checkedAt: 1_000_000 });
|
|
199
|
-
await maybeNudge(baseDeps({ isTty: false }));
|
|
200
|
-
expect(stderr.text).toBe("");
|
|
201
|
-
expect(fetchOk).not.toHaveBeenCalled();
|
|
202
|
-
});
|
|
203
|
-
it("suppresses the nudge from within `sweny upgrade` itself", async () => {
|
|
204
|
-
writeCache(cachePath, { latest: "0.1.66", checkedAt: 1_000_000 });
|
|
205
|
-
await maybeNudge(baseDeps({ commandName: "upgrade" }));
|
|
206
|
-
expect(stderr.text).toBe("");
|
|
207
|
-
expect(fetchOk).not.toHaveBeenCalled();
|
|
208
|
-
});
|
|
209
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|