@rightkit/release 0.2.68 → 0.2.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/cli/right-release.mjs +0 -0
  2. package/package.json +13 -9
  3. package/release.mjs +47 -14
  4. package/rightkit-versions.json +4 -2
  5. package/addon-command.test.mjs +0 -54
  6. package/addon-contract.test.mjs +0 -48
  7. package/asr-artifact-adoption.test.mjs +0 -122
  8. package/build-invocation-contract.test.mjs +0 -124
  9. package/build-release.test.mjs +0 -242
  10. package/cache-command.test.mjs +0 -118
  11. package/cache-policy.test.mjs +0 -346
  12. package/cargo-guard.test.mjs +0 -195
  13. package/cargo-target.test.mjs +0 -82
  14. package/create-mac-updater.test.mjs +0 -14
  15. package/github-release.test.mjs +0 -103
  16. package/heavy-command.test.mjs +0 -221
  17. package/legal-contract.test.mjs +0 -151
  18. package/mirror-root-artifact.test.mjs +0 -58
  19. package/model-promote.test.mjs +0 -284
  20. package/notary-auth.test.mjs +0 -31
  21. package/nsis-payload.test.mjs +0 -139
  22. package/nsis-upgrade-contract.test.mjs +0 -57
  23. package/preflight.test.mjs +0 -123
  24. package/progress-control.test.mjs +0 -56
  25. package/prune-r2.test.mjs +0 -12
  26. package/publish-cargo.test.mjs +0 -43
  27. package/publish-swift.test.mjs +0 -44
  28. package/publish-update.test.mjs +0 -207
  29. package/qa-contract.test.mjs +0 -47
  30. package/registry-parity.test.mjs +0 -30
  31. package/release-cli-contract.test.mjs +0 -119
  32. package/release-invocation.test.mjs +0 -22
  33. package/release-state.test.mjs +0 -395
  34. package/release-token.test.mjs +0 -21
  35. package/release.test.mjs +0 -477
  36. package/right-suite-contract.test.mjs +0 -1007
  37. package/rightapps-register.test.mjs +0 -28
  38. package/runtime-artifact-manifest.test.mjs +0 -128
  39. package/sign-updater.test.mjs +0 -12
  40. package/source-gate.test.mjs +0 -25
  41. package/standalone-clone-evidence.json +0 -32
  42. package/standalone-clone-verify.test.mjs +0 -76
  43. package/suite-doctor.test.mjs +0 -19
  44. package/target-bridge.test.mjs +0 -269
  45. package/tauri-bundle-marker.test.mjs +0 -81
  46. package/upload-large.test.mjs +0 -70
@@ -1,284 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { createHash, generateKeyPairSync, sign } from "node:crypto";
3
- import { spawnSync } from "node:child_process";
4
- import { mkdtemp, rm, writeFile } from "node:fs/promises";
5
- import os from "node:os";
6
- import path from "node:path";
7
- import test from "node:test";
8
- import { fileURLToPath } from "node:url";
9
-
10
- import {
11
- RUNTIME_ARTIFACT_TRUSTED_KEY_ID,
12
- defaultWindowsRuntimeArtifactSigningKeyFile,
13
- loadRuntimeArtifactSigningKey,
14
- promoteRuntimeArtifact,
15
- runtimeArtifactUploadEnv,
16
- } from "./model-promote.mjs";
17
- const { privateKey } = generateKeyPairSync("ed25519");
18
- const artifactDigest = "a".repeat(64);
19
- const evidenceDigest = "b".repeat(64);
20
- const packageRoot = path.dirname(fileURLToPath(import.meta.url));
21
- const candidateCommit = "a".repeat(40);
22
-
23
- // Source identity as build/upload/promote read it: the exact commit, plus the
24
- // porcelain status that must be empty for the promotion to run.
25
- function verificationFixture(commit = candidateCommit, status = "") {
26
- return { candidateCommit: commit, status };
27
- }
28
-
29
- function request(overrides = {}) {
30
- return {
31
- authority: "heardright",
32
- privateKey,
33
- keyId: RUNTIME_ARTIFACT_TRUSTED_KEY_ID,
34
- artifactFile: "C:/fixtures/parakeet.onnx",
35
- evidenceFile: "C:/fixtures/eval.json",
36
- config: {
37
- artifact: {
38
- filename: "parakeet.onnx",
39
- sha256: artifactDigest,
40
- sizeBytes: 42,
41
- },
42
- evidence: { sha256: evidenceDigest },
43
- manifest: {
44
- artifactKind: "asr-model",
45
- entitlement: { appKey: "scraperight", tier: "pro" },
46
- distribution: { delivery: "private-r2", bucket: "rightapps-updates" },
47
- target: { os: "windows", arch: "x86_64" },
48
- versions: {
49
- runtime: "rightkit-asr-0.1.0",
50
- model: "parakeet-tdt-0.6b-v3",
51
- tokenizer: "sentencepiece-2026-07-14",
52
- preprocessing: "rightkit-asr-0.1.0",
53
- license: "cc-by-4.0",
54
- provenance: "heardright-approved-2026-07-14",
55
- },
56
- provenance: {
57
- source: "https://huggingface.co/nvidia/parakeet-tdt-0.6b-v3",
58
- sourceRevision: "0123456789abcdef",
59
- licenseId: "CC-BY-4.0",
60
- noticeSha256: "c".repeat(64),
61
- },
62
- promotion: {
63
- promotionId: "hr-asr-2026-07-14-001",
64
- promotedAt: "2026-07-14T12:00:00.000Z",
65
- },
66
- },
67
- },
68
- verification: verificationFixture(),
69
- ...overrides,
70
- };
71
- }
72
-
73
- function boundaries(events) {
74
- return {
75
- metadata: async (file) => file.endsWith("eval.json")
76
- ? { sha256: evidenceDigest, sizeBytes: 10 }
77
- : { sha256: artifactDigest, sizeBytes: 42 },
78
- upload: async ({ purpose }) => { events.push(`upload:${purpose}`); },
79
- register: async () => { events.push("register"); },
80
- };
81
- }
82
-
83
- test("uploads immutable object, registers signed envelope, then replaces stable pointer", async () => {
84
- const events = [];
85
- const result = await promoteRuntimeArtifact(request(), boundaries(events));
86
-
87
- assert.deepEqual(events, ["upload:object", "register", "upload:pointer"]);
88
- assert.equal(result.envelope.signature.keyId, RUNTIME_ARTIFACT_TRUSTED_KEY_ID);
89
- assert.equal(result.envelope.manifest.object.sha256, artifactDigest);
90
- assert.equal(result.envelope.manifest.promotion.evidenceSha256, evidenceDigest);
91
- assert.match(result.envelope.manifest.pointerKey, /\/current\/manifest\.json$/);
92
- });
93
-
94
- test("dry-run validates and signs without invoking process or network boundaries", async () => {
95
- const events = [];
96
- const result = await promoteRuntimeArtifact(request({ dryRun: true }), boundaries(events));
97
- assert.deepEqual(events, []);
98
- assert.deepEqual(result.steps, ["upload immutable object", "register signed envelope", "replace stable pointer"]);
99
- assert.equal(result.envelope.signature.algorithm, "Ed25519");
100
- });
101
-
102
- test("model promotion refuses a dirty working tree", async () => {
103
- const events = [];
104
- const verification = verificationFixture(candidateCommit, " M packages/release/model-promote.mjs\0");
105
- await assert.rejects(
106
- promoteRuntimeArtifact(request({ verification, dryRun: true }), boundaries(events)),
107
- /clean working tree/,
108
- );
109
- assert.deepEqual(events, []);
110
- });
111
-
112
- test("model promotion requires source identity", async () => {
113
- const events = [];
114
- await assert.rejects(
115
- promoteRuntimeArtifact(request({ verification: undefined, dryRun: true }), boundaries(events)),
116
- /source identity is required/,
117
- );
118
- assert.deepEqual(events, []);
119
- });
120
-
121
- test("never registers or publishes a pointer after immutable upload failure", async () => {
122
- const events = [];
123
- const deps = boundaries(events);
124
- deps.upload = async ({ purpose }) => {
125
- events.push(`upload:${purpose}`);
126
- throw new Error("R2 failed");
127
- };
128
- await assert.rejects(promoteRuntimeArtifact(request(), deps), /R2 failed/);
129
- assert.deepEqual(events, ["upload:object"]);
130
- });
131
-
132
- test("never publishes a pointer when RightApps registration fails", async () => {
133
- const events = [];
134
- const deps = boundaries(events);
135
- deps.register = async () => {
136
- events.push("register");
137
- throw new Error("registration failed");
138
- };
139
- await assert.rejects(promoteRuntimeArtifact(request(), deps), /registration failed/);
140
- assert.deepEqual(events, ["upload:object", "register"]);
141
- });
142
-
143
- test("fails before mutation on digest, authority, key-id, or lane contradiction", async () => {
144
- const badRequests = [
145
- request({ authority: "scraperight" }),
146
- request({ keyId: "untrusted-key" }),
147
- request({ config: { ...request().config, artifact: { ...request().config.artifact, sha256: "d".repeat(64) } } }),
148
- request({ config: {
149
- ...request().config,
150
- manifest: {
151
- ...request().config.manifest,
152
- entitlement: { appKey: "scraperight", tier: "public" },
153
- distribution: { delivery: "public-r2", bucket: "rightapps-downloads" },
154
- },
155
- } }),
156
- request({ config: {
157
- ...request().config,
158
- manifest: {
159
- ...request().config.manifest,
160
- promotion: {
161
- ...request().config.manifest.promotion,
162
- authorityAppKey: "scraperight",
163
- },
164
- },
165
- } }),
166
- ];
167
- for (const input of badRequests) {
168
- const events = [];
169
- await assert.rejects(promoteRuntimeArtifact(input, boundaries(events)));
170
- assert.deepEqual(events, []);
171
- }
172
- });
173
-
174
- test("right-release routes model promote to the portable promotion lane", () => {
175
- const result = spawnSync(process.execPath, [path.join(packageRoot, "cli/right-release.mjs"), "model", "promote", "--authority", "heardright"], {
176
- cwd: packageRoot,
177
- encoding: "utf8",
178
- windowsHide: true,
179
- });
180
- assert.equal(result.status, 1);
181
- assert.match(result.stderr, /model promote requires --config/);
182
- assert.doesNotMatch(result.stderr, /unknown command/);
183
- });
184
-
185
- test("model promote help documents the portable config and secure key inputs", () => {
186
- const result = spawnSync(process.execPath, [path.join(packageRoot, "cli/right-release.mjs"), "model", "promote", "--help"], {
187
- cwd: packageRoot,
188
- encoding: "utf8",
189
- windowsHide: true,
190
- });
191
- assert.equal(result.status, 0, result.stderr);
192
- assert.match(result.stdout, /artifact\.file/);
193
- assert.match(result.stdout, /evidence\.sha256/);
194
- assert.match(result.stdout, /RIGHTKIT_RUNTIME_ARTIFACT_PRIVATE_KEY_FILE/);
195
- assert.match(result.stdout, /RIGHTAPPS_RELEASE_TOKEN/);
196
- });
197
-
198
- test("portable CLI promotes from a clean checkout, refuses a dirty one, and rejects injected trust flags", async () => {
199
- const root = await mkdtemp(path.join(os.tmpdir(), "rightkit-model-promote-test-"));
200
- try {
201
- const artifact = Buffer.from("tiny model fixture");
202
- const evidence = Buffer.from('{"certified":true}');
203
- const testKeys = generateKeyPairSync("ed25519");
204
- const keyPem = testKeys.privateKey.export({ type: "pkcs8", format: "pem" }).toString();
205
- await Promise.all([
206
- writeFile(path.join(root, "model.onnx"), artifact),
207
- writeFile(path.join(root, "evidence.json"), evidence),
208
- writeFile(path.join(root, "key.pem"), keyPem, { mode: 0o600 }),
209
- ]);
210
- spawnSync("git", ["init"], { cwd: root, encoding: "utf8" });
211
- spawnSync("git", ["add", "."], { cwd: root, encoding: "utf8" });
212
- const committed = spawnSync("git", ["-c", "user.name=fixture", "-c", "user.email=fixture@example.invalid", "commit", "-m", "fixture"], { cwd: root, encoding: "utf8" });
213
- assert.equal(committed.status, 0, committed.stderr);
214
- const config = {
215
- artifact: {
216
- file: "model.onnx",
217
- filename: "model.onnx",
218
- sha256: createHash("sha256").update(artifact).digest("hex"),
219
- sizeBytes: artifact.length,
220
- },
221
- evidence: {
222
- file: "evidence.json",
223
- sha256: createHash("sha256").update(evidence).digest("hex"),
224
- },
225
- manifest: request().config.manifest,
226
- };
227
- await writeFile(path.join(root, "promotion.json"), JSON.stringify(config));
228
- spawnSync("git", ["add", "."], { cwd: root, encoding: "utf8" });
229
- spawnSync("git", ["-c", "user.name=fixture", "-c", "user.email=fixture@example.invalid", "commit", "-m", "config"], { cwd: root, encoding: "utf8" });
230
-
231
- const promoteArgs = [
232
- path.join(packageRoot, "cli/right-release.mjs"), "model", "promote",
233
- "--authority", "heardright",
234
- "--config", path.join(root, "promotion.json"),
235
- "--signing-key-file", path.join(root, "key.pem"),
236
- "--dry-run",
237
- ];
238
- const result = spawnSync(process.execPath, promoteArgs, { cwd: root, encoding: "utf8", windowsHide: true });
239
- assert.equal(result.status, 0, result.stderr);
240
-
241
- await writeFile(path.join(root, "untracked.txt"), "dirty");
242
- const dirty = spawnSync(process.execPath, promoteArgs, { cwd: root, encoding: "utf8", windowsHide: true });
243
- assert.equal(dirty.status, 1);
244
- assert.match(dirty.stderr, /clean working tree/);
245
-
246
- const attacker = spawnSync(process.execPath, [
247
- path.join(packageRoot, "cli/right-release.mjs"), "model", "promote",
248
- "--authority", "heardright", "--config", path.join(root, "promotion.json"),
249
- "--verification-public-key", path.join(root, "verification-public.pem"),
250
- ], { cwd: root, encoding: "utf8", windowsHide: true });
251
- assert.equal(attacker.status, 1);
252
- assert.match(attacker.stderr, /unknown model promote argument: --verification-public-key/);
253
- assert.doesNotMatch(`${result.stdout}${result.stderr}`, /BEGIN PRIVATE KEY/);
254
- assert.doesNotMatch(`${result.stdout}${result.stderr}`, /Uploading|POST https:/);
255
- } finally {
256
- await rm(root, { recursive: true, force: true });
257
- }
258
- });
259
-
260
- test("uses the canonical Windows signing-key path and supports a Keychain-fed PEM environment value", async () => {
261
- assert.equal(
262
- defaultWindowsRuntimeArtifactSigningKeyFile({ APPDATA: "C:/Users/test/AppData/Roaming" }),
263
- path.join("C:/Users/test/AppData/Roaming", "RightKit", "runtime-artifact-signing-key.pem"),
264
- );
265
- assert.equal(
266
- await loadRuntimeArtifactSigningKey({ env: { RIGHTKIT_RUNTIME_ARTIFACT_PRIVATE_KEY: "line1\\nline2" }, platform: "darwin" }),
267
- "line1\nline2",
268
- );
269
- });
270
-
271
- test("does not forward signing or RightApps credentials into the Wrangler uploader process", () => {
272
- const env = runtimeArtifactUploadEnv({
273
- CLOUDFLARE_API_TOKEN: "cloudflare-token",
274
- RIGHTKIT_RUNTIME_ARTIFACT_PRIVATE_KEY: "private-pem",
275
- RIGHTKIT_RUNTIME_ARTIFACT_PRIVATE_KEY_FILE: "key.pem",
276
- RIGHTAPPS_RELEASE_TOKEN: "release-token",
277
- RIGHTAPPS_RELEASE_TOKEN_FILE: "release.token",
278
- });
279
- assert.equal(env.CLOUDFLARE_API_TOKEN, "cloudflare-token");
280
- assert.equal(env.RIGHTKIT_RUNTIME_ARTIFACT_PRIVATE_KEY, undefined);
281
- assert.equal(env.RIGHTKIT_RUNTIME_ARTIFACT_PRIVATE_KEY_FILE, undefined);
282
- assert.equal(env.RIGHTAPPS_RELEASE_TOKEN, undefined);
283
- assert.equal(env.RIGHTAPPS_RELEASE_TOKEN_FILE, undefined);
284
- });
@@ -1,31 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import test from "node:test";
5
- import { notarytoolAuthArgs } from "./notary-auth.mjs";
6
-
7
- test("prefers a complete App Store Connect API credential set", () => {
8
- const before = { ...process.env };
9
- try {
10
- process.env.APPLE_API_KEY_PATH = "~/AuthKey.p8";
11
- process.env.APPLE_API_KEY = "KEY123";
12
- process.env.APPLE_API_ISSUER = "ISSUER123";
13
- assert.deepEqual(notarytoolAuthArgs(), ["--key", path.join(os.homedir(), "AuthKey.p8"), "--key-id", "KEY123", "--issuer", "ISSUER123"]);
14
- } finally {
15
- process.env = before;
16
- }
17
- });
18
-
19
- test("falls back to the device keychain profile and rejects partial API auth", () => {
20
- const before = { ...process.env };
21
- try {
22
- delete process.env.APPLE_API_KEY_PATH;
23
- delete process.env.APPLE_API_KEY;
24
- delete process.env.APPLE_API_ISSUER;
25
- assert.deepEqual(notarytoolAuthArgs(), ["--keychain-profile", "apple-dev-notary"]);
26
- process.env.APPLE_API_KEY = "KEY123";
27
- assert.throws(() => notarytoolAuthArgs(), /requires APPLE_API_KEY_PATH.*APPLE_API_ISSUER/);
28
- } finally {
29
- process.env = before;
30
- }
31
- });
@@ -1,139 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { createHash } from "node:crypto";
3
- import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
- import { tmpdir } from "node:os";
5
- import path from "node:path";
6
- import test, { after } from "node:test";
7
- import { listNsisPayload, parseSevenZipListing, resolveSevenZip, verifyNsisEmbeddedBinary } from "./nsis-payload.mjs";
8
-
9
- const workspace = mkdtempSync(path.join(tmpdir(), "rightkit-nsis-"));
10
- after(() => rmSync(workspace, { recursive: true, force: true }));
11
-
12
- // 7-Zip's technical listing: an archive record, a rule, then one record per member.
13
- // The blank `Attributes` and absent timestamps are what the column-aligned listing
14
- // gets wrong, so they are reproduced here deliberately.
15
- const LISTING = [
16
- "Path = D:\\out\\App_1.2.3_x64-setup.exe",
17
- "Type = Nsis",
18
- "",
19
- "----------",
20
- "Path = $PLUGINSDIR\\System.dll",
21
- "Size = 12288",
22
- "Attributes = ",
23
- "",
24
- "Path = app with spaces.exe",
25
- "Size = 13750736",
26
- "Attributes = ",
27
- "",
28
- "Path = legal",
29
- "Size = 0",
30
- "Folder = +",
31
- "Attributes = D",
32
- "",
33
- "Path = legal\\EULA.md",
34
- "Size = 16723",
35
- "Attributes = A",
36
- "",
37
- ].join("\n");
38
-
39
- test("parses 7-Zip technical listings, including blank dates and spaced names", () => {
40
- const entries = parseSevenZipListing(LISTING);
41
- assert.deepEqual(entries.map((entry) => entry.name), [
42
- "$PLUGINSDIR/System.dll",
43
- "app with spaces.exe",
44
- "legal/EULA.md",
45
- ]);
46
- assert.equal(entries[1].sizeBytes, 13_750_736);
47
- });
48
-
49
- test("ignores the archive's own record above the rule", () => {
50
- const entries = parseSevenZipListing(LISTING);
51
- assert.equal(entries.some((entry) => entry.name.endsWith("setup.exe")), false);
52
- });
53
-
54
- test("listNsisPayload surfaces the 7-Zip failure instead of reporting an empty payload", () => {
55
- assert.throws(
56
- () => listNsisPayload("installer.exe", {
57
- sevenZip: "7z",
58
- run: () => ({ status: 2, stdout: "", stderr: "Cannot open the file as archive" }),
59
- }),
60
- /could not read the NSIS payload.*Cannot open the file as archive/s,
61
- );
62
- });
63
-
64
- test("verifyNsisEmbeddedBinary accepts a payload whose bytes match the signed EXE", () => {
65
- const installer = path.join(workspace, "setup.exe");
66
- writeFileSync(installer, "installer-container");
67
- const payload = Buffer.from("signed-executable-bytes");
68
- const sha256 = createHash("sha256").update(payload).digest("hex");
69
-
70
- const receipt = verifyNsisEmbeddedBinary({
71
- installer,
72
- entryName: "app.exe",
73
- expectedSha256: sha256,
74
- sevenZip: "7z",
75
- run: (_command, args) => {
76
- const destination = args.find((arg) => arg.startsWith("-o")).slice(2);
77
- writeFileSync(path.join(destination, "app.exe"), payload);
78
- return { status: 0, stdout: "Everything is Ok", stderr: "" };
79
- },
80
- });
81
-
82
- assert.equal(receipt.sha256, sha256);
83
- assert.equal(receipt.entry, "app.exe");
84
- assert.equal(receipt.sizeBytes, payload.length);
85
- assert.equal(receipt.installerSha256, createHash("sha256").update(readFileSync(installer)).digest("hex"));
86
- });
87
-
88
- test("verifyNsisEmbeddedBinary rejects a payload that drifted from the signed EXE", () => {
89
- const installer = path.join(workspace, "setup-drifted.exe");
90
- writeFileSync(installer, "installer-container");
91
- assert.throws(
92
- () => verifyNsisEmbeddedBinary({
93
- installer,
94
- entryName: "app.exe",
95
- expectedSha256: createHash("sha256").update("signed-executable-bytes").digest("hex"),
96
- sevenZip: "7z",
97
- run: (_command, args) => {
98
- const destination = args.find((arg) => arg.startsWith("-o")).slice(2);
99
- writeFileSync(path.join(destination, "app.exe"), "rewritten-after-signing");
100
- return { status: 0, stdout: "Everything is Ok", stderr: "" };
101
- },
102
- }),
103
- /does not embed the signed app\.exe.*Authenticode signature is invalid/s,
104
- );
105
- });
106
-
107
- test("verifyNsisEmbeddedBinary fails when the entry is absent rather than passing vacuously", () => {
108
- const installer = path.join(workspace, "setup-missing.exe");
109
- writeFileSync(installer, "installer-container");
110
- assert.throws(
111
- () => verifyNsisEmbeddedBinary({
112
- installer,
113
- entryName: "app.exe",
114
- expectedSha256: "a".repeat(64),
115
- sevenZip: "7z",
116
- run: () => ({ status: 2, stdout: "", stderr: "No files to process" }),
117
- }),
118
- /could not extract app\.exe/,
119
- );
120
- });
121
-
122
- test("verifyNsisEmbeddedBinary refuses a caller that has no digest to compare against", () => {
123
- assert.throws(
124
- () => verifyNsisEmbeddedBinary({ installer: "setup.exe", entryName: "app.exe", expectedSha256: undefined, sevenZip: "7z", run: () => ({ status: 0 }) }),
125
- /expected a sha256 digest/,
126
- );
127
- });
128
-
129
- test("resolveSevenZip names the fix when 7-Zip is absent", () => {
130
- assert.throws(
131
- () => resolveSevenZip({ env: {}, exists: () => false, which: () => null }),
132
- /install 7-Zip.*RIGHT_RELEASE_SEVENZIP/s,
133
- );
134
- });
135
-
136
- test("resolveSevenZip honours an explicit override and rejects a bad one", () => {
137
- assert.equal(resolveSevenZip({ env: { RIGHT_RELEASE_SEVENZIP: "C:\\tools\\7z.exe" }, exists: () => true }), "C:\\tools\\7z.exe");
138
- assert.throws(() => resolveSevenZip({ env: { RIGHT_RELEASE_SEVENZIP: "C:\\gone\\7z.exe" }, exists: () => false }), /does not exist/);
139
- });
@@ -1,57 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import test from "node:test";
6
- import { assertNsisInPlaceUpgradeContract } from "./nsis-upgrade-contract.mjs";
7
-
8
- function fixture(template) {
9
- const root = mkdtempSync(path.join(os.tmpdir(), "right-nsis-upgrade-"));
10
- const tauriDir = path.join(root, "src-tauri");
11
- const windowsDir = path.join(tauriDir, "windows");
12
- mkdirSync(windowsDir, { recursive: true });
13
- writeFileSync(
14
- path.join(tauriDir, "tauri.conf.json"),
15
- `${JSON.stringify({ bundle: { windows: { nsis: { template: "windows/installer.nsi" } } } })}\n`,
16
- );
17
- writeFileSync(path.join(windowsDir, "installer.nsi"), template);
18
- return root;
19
- }
20
-
21
- test("rejects an NSIS template that exposes uninstall-or-reinstall maintenance choices", () => {
22
- const root = fixture(`
23
- Page custom PageReinstall PageLeaveReinstall
24
- Function PageReinstall
25
- nsDialogs::Show
26
- FunctionEnd
27
- `);
28
- assert.throws(
29
- () => assertNsisInPlaceUpgradeContract(root),
30
- /automatic in-place upgrade|maintenance choice/i,
31
- );
32
- });
33
-
34
- test("accepts a template that skips maintenance UI for same-version repair and upgrades", () => {
35
- const root = fixture(`
36
- !define RIGHTKIT_AUTOMATIC_IN_PLACE_UPGRADE
37
- Function PageReinstall
38
- $\{If\} $WixMode = 0
39
- $\{AndIf\} $R0 >= 0
40
- Abort
41
- $\{EndIf\}
42
- nsDialogs::Show
43
- FunctionEnd
44
- `);
45
- const result = assertNsisInPlaceUpgradeContract(root);
46
- assert.match(result.templatePath, /installer\.nsi$/);
47
- });
48
-
49
- test("rejects a marker without executable skip-page logic", () => {
50
- const root = fixture(`
51
- !define RIGHTKIT_AUTOMATIC_IN_PLACE_UPGRADE
52
- Function PageReinstall
53
- nsDialogs::Show
54
- FunctionEnd
55
- `);
56
- assert.throws(() => assertNsisInPlaceUpgradeContract(root), /skip.*logic/i);
57
- });
@@ -1,123 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import path from "node:path";
4
-
5
- import {
6
- buildPathPrefix,
7
- collectPreflight,
8
- findWindowsSdkTool,
9
- formatPreflight,
10
- graphVendorsOpenssl,
11
- nextFreeVersion,
12
- preflightFailures,
13
- resolveWindowsPerl,
14
- PREFLIGHT_FAIL,
15
- } from "./preflight.mjs";
16
-
17
- test("windows SDK lookup orders versions numerically, not as strings", () => {
18
- const root = "C:\\Kits";
19
- const present = new Set([
20
- root,
21
- path.join(root, "10.0.9.0", "x64", "makeappx.exe"),
22
- path.join(root, "10.0.26100.0", "x64", "makeappx.exe"),
23
- ]);
24
- const found = findWindowsSdkTool("makeappx.exe", {
25
- sdkRoot: root,
26
- readdir: () => ["10.0.9.0", "10.0.26100.0"],
27
- exists: (candidate) => present.has(candidate),
28
- });
29
- // String sort would pick 10.0.9.0 — that is the bug this guards.
30
- assert.equal(found.version, "10.0.26100.0");
31
- });
32
-
33
- test("windows SDK lookup returns null when the tool is absent from every SDK", () => {
34
- const found = findWindowsSdkTool("makeappx.exe", {
35
- sdkRoot: "C:\\Kits",
36
- readdir: () => ["10.0.26100.0"],
37
- exists: (candidate) => candidate === "C:\\Kits",
38
- });
39
- assert.equal(found, null);
40
- });
41
-
42
- test("explicit OPENSSL_SRC_PERL wins over the default Strawberry location", () => {
43
- const resolved = resolveWindowsPerl({
44
- env: { OPENSSL_SRC_PERL: "D:\\perl\\bin\\perl.exe" },
45
- exists: () => true,
46
- });
47
- assert.equal(resolved.perlPath, "D:\\perl\\bin\\perl.exe");
48
- assert.equal(resolved.dir, "D:\\perl\\bin");
49
- });
50
-
51
- test("perl resolution reports nothing when no windows perl exists", () => {
52
- assert.equal(resolveWindowsPerl({ env: {}, exists: () => false }), null);
53
- });
54
-
55
- test("vendored openssl is detected from the cargo lock", () => {
56
- assert.equal(
57
- graphVendorsOpenssl("Cargo.lock", { exists: () => true, read: () => 'name = "openssl-src"\n' }),
58
- true,
59
- );
60
- assert.equal(
61
- graphVendorsOpenssl("Cargo.lock", { exists: () => true, read: () => 'name = "rustls"\n' }),
62
- false,
63
- );
64
- assert.equal(graphVendorsOpenssl("missing.lock", { exists: () => false }), false);
65
- });
66
-
67
- test("a sealed version collision suggests the next free patch, skipping taken ones", () => {
68
- const result = nextFreeVersion("/sealed", "heardright", "0.1.33", {
69
- exists: () => true,
70
- readdir: () => [
71
- "heardright-0.1.33-6f2f5e64",
72
- "heardright-0.1.34-839ad700",
73
- "heardright-0.1.36-f7035acc",
74
- "viewright-0.1.35-aaaaaaaa",
75
- ],
76
- });
77
- assert.equal(result.collision, true);
78
- // 0.1.34 is taken, 0.1.35 is free — and viewright's record must not be counted.
79
- assert.equal(result.suggestion, "0.1.35");
80
- });
81
-
82
- test("an unsealed version is reported as free", () => {
83
- const result = nextFreeVersion("/sealed", "heardright", "0.1.37", {
84
- exists: () => true,
85
- readdir: () => ["heardright-0.1.33-6f2f5e64"],
86
- });
87
- assert.equal(result.collision, false);
88
- assert.equal(result.suggestion, "0.1.37");
89
- });
90
-
91
- test("a sealed version collision is a hard failure carrying a concrete fix", () => {
92
- const checks = collectPreflight({
93
- platform: "linux",
94
- repoRoot: process.cwd(),
95
- app: "nothing-is-sealed-here",
96
- version: "9.9.9",
97
- });
98
- // No sealed dir for this app, so the version check must pass rather than throw.
99
- const version = checks.find((check) => check.name === "version");
100
- assert.equal(version.status, "ok");
101
- });
102
-
103
- test("formatting surfaces the fix line only for non-ok checks", () => {
104
- const text = formatPreflight([
105
- { name: "a", status: "ok", detail: "fine", fix: "unused" },
106
- { name: "b", status: PREFLIGHT_FAIL, detail: "broken", fix: "do the thing" },
107
- ]);
108
- assert.match(text, /\[ok {2}\] a: fine/);
109
- assert.doesNotMatch(text, /fix: unused/);
110
- assert.match(text, /fix: do the thing/);
111
- });
112
-
113
- test("failures are filtered from warnings so warnings never block a build", () => {
114
- const failures = preflightFailures([
115
- { name: "a", status: "warn", detail: "" },
116
- { name: "b", status: PREFLIGHT_FAIL, detail: "" },
117
- ]);
118
- assert.deepEqual(failures.map((check) => check.name), ["b"]);
119
- });
120
-
121
- test("build PATH prefix is windows-only", () => {
122
- assert.equal(buildPathPrefix({ platform: "mac" }), null);
123
- });