Release ForgeFlow 0.8.2 with binary auto-update
This commit is contained in:
+346
-108
@@ -1,66 +1,95 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtemp, rm, mkdir, writeFile, readFile } from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { createRequire } from 'node:module';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, rm, mkdir, writeFile, readFile } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { createRequire } from "node:module";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { createHash } from "node:crypto";
|
||||
const require = createRequire(import.meta.url);
|
||||
const { UpdateService, waitForUpdaterStarted } = require('../src/main/update-service.cjs');
|
||||
const {
|
||||
UpdateService,
|
||||
waitForUpdaterStarted,
|
||||
} = require("../src/main/update-service.cjs");
|
||||
|
||||
test('update check pins version to an exact branch commit', async () => {
|
||||
const temp = await mkdtemp(path.join(os.tmpdir(), 'forgeflow-update-test-'));
|
||||
test("update check pins version to an exact branch commit", async () => {
|
||||
const temp = await mkdtemp(path.join(os.tmpdir(), "forgeflow-update-test-"));
|
||||
const saved = [];
|
||||
const store = {
|
||||
data: {
|
||||
gitea: { baseUrl: 'https://gitea.example.test' },
|
||||
updates: { owner: 'Jens', repo: 'ForgeFlow', branch: 'main', autoCheck: true }
|
||||
gitea: { baseUrl: "https://gitea.example.test" },
|
||||
updates: {
|
||||
owner: "Jens",
|
||||
repo: "ForgeFlow",
|
||||
branch: "main",
|
||||
autoCheck: true,
|
||||
},
|
||||
},
|
||||
async save() {
|
||||
saved.push(true);
|
||||
},
|
||||
async save() { saved.push(true); }
|
||||
};
|
||||
const calls = [];
|
||||
const gitea = {
|
||||
async getBranch(owner, repo, branch) {
|
||||
calls.push(['branch', owner, repo, branch]);
|
||||
return { commit: { id: 'a'.repeat(40) } };
|
||||
calls.push(["branch", owner, repo, branch]);
|
||||
return { commit: { id: "a".repeat(40) } };
|
||||
},
|
||||
async getRepositoryFile(input) {
|
||||
calls.push(['file', input]);
|
||||
return { decoded: JSON.stringify({ name: 'forgeflow', version: '0.4.1' }) };
|
||||
}
|
||||
calls.push(["file", input]);
|
||||
return {
|
||||
decoded: JSON.stringify({ name: "forgeflow", version: "0.4.1" }),
|
||||
};
|
||||
},
|
||||
};
|
||||
const service = new UpdateService({
|
||||
store, gitea, diagnostics: null,
|
||||
appInfo: { version: '0.4.0', packaged: false },
|
||||
sourcePath: temp, userDataPath: temp
|
||||
store,
|
||||
gitea,
|
||||
diagnostics: null,
|
||||
appInfo: { version: "0.4.0", packaged: false },
|
||||
sourcePath: temp,
|
||||
userDataPath: temp,
|
||||
});
|
||||
const result = await service.check();
|
||||
assert.equal(result.available, true);
|
||||
assert.equal(result.remoteSha, 'a'.repeat(40));
|
||||
assert.equal(calls[1][1].ref, 'a'.repeat(40));
|
||||
assert.equal(result.remoteSha, "a".repeat(40));
|
||||
assert.equal(calls[1][1].ref, "a".repeat(40));
|
||||
assert.equal(saved.length, 1);
|
||||
await rm(temp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('update repository parts reject path injection', async () => {
|
||||
const temp = await mkdtemp(path.join(os.tmpdir(), 'forgeflow-update-test-'));
|
||||
test("update repository parts reject path injection", async () => {
|
||||
const temp = await mkdtemp(path.join(os.tmpdir(), "forgeflow-update-test-"));
|
||||
const service = new UpdateService({
|
||||
store: { data: { updates: { owner: '../Jens', repo: 'ForgeFlow', branch: 'main' } }, save: async () => {} },
|
||||
gitea: {}, diagnostics: null, appInfo: { version: '0.4.0', packaged: false }, sourcePath: temp, userDataPath: temp
|
||||
store: {
|
||||
data: {
|
||||
updates: { owner: "../Jens", repo: "ForgeFlow", branch: "main" },
|
||||
},
|
||||
save: async () => {},
|
||||
},
|
||||
gitea: {},
|
||||
diagnostics: null,
|
||||
appInfo: { version: "0.4.0", packaged: false },
|
||||
sourcePath: temp,
|
||||
userDataPath: temp,
|
||||
});
|
||||
await assert.rejects(() => service.check(), /unsupported characters/);
|
||||
await rm(temp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
|
||||
test('source updater confirms an external STARTED marker before ForgeFlow may close', async () => {
|
||||
const temp = await mkdtemp(path.join(os.tmpdir(), 'forgeflow-update-handshake-'));
|
||||
const source = path.join(temp, 'source');
|
||||
const scripts = path.join(source, 'scripts');
|
||||
const archive = path.join(temp, 'update.zip');
|
||||
test("source updater confirms an external STARTED marker before ForgeFlow may close", async () => {
|
||||
const temp = await mkdtemp(
|
||||
path.join(os.tmpdir(), "forgeflow-update-handshake-"),
|
||||
);
|
||||
const source = path.join(temp, "source");
|
||||
const scripts = path.join(source, "scripts");
|
||||
const archive = path.join(temp, "update.zip");
|
||||
await mkdir(scripts, { recursive: true });
|
||||
await writeFile(path.join(scripts, 'apply-source-update.ps1'), '# test helper');
|
||||
await writeFile(archive, 'PK fake archive');
|
||||
await writeFile(
|
||||
path.join(scripts, "apply-source-update.ps1"),
|
||||
"# test helper",
|
||||
);
|
||||
await writeFile(archive, "PK fake archive");
|
||||
|
||||
let capturedArgs = null;
|
||||
const spawnProcess = (_command, args) => {
|
||||
@@ -68,71 +97,116 @@ test('source updater confirms an external STARTED marker before ForgeFlow may cl
|
||||
const child = new EventEmitter();
|
||||
child.pid = 4321;
|
||||
child.unref = () => {};
|
||||
queueMicrotask(() => child.emit('spawn'));
|
||||
const statusIndex = args.indexOf('-StatusPath');
|
||||
queueMicrotask(() => child.emit("spawn"));
|
||||
const statusIndex = args.indexOf("-StatusPath");
|
||||
const statusPath = args[statusIndex + 1];
|
||||
const updateIdIndex = args.indexOf('-UpdateId');
|
||||
const updateIdIndex = args.indexOf("-UpdateId");
|
||||
const updateId = args[updateIdIndex + 1];
|
||||
setTimeout(() => writeFile(statusPath, JSON.stringify({ state: 'started', expectedVersion: '0.5.3', updateId })), 30);
|
||||
setTimeout(
|
||||
() =>
|
||||
writeFile(
|
||||
statusPath,
|
||||
JSON.stringify({
|
||||
state: "started",
|
||||
expectedVersion: "0.5.3",
|
||||
updateId,
|
||||
}),
|
||||
),
|
||||
30,
|
||||
);
|
||||
return child;
|
||||
};
|
||||
|
||||
const service = new UpdateService({
|
||||
store: { data: { updates: {}, gitea: { baseUrl: 'https://example.test' } }, save: async () => {} },
|
||||
gitea: {}, diagnostics: null,
|
||||
appInfo: { version: '0.5.2', packaged: false },
|
||||
store: {
|
||||
data: { updates: {}, gitea: { baseUrl: "https://example.test" } },
|
||||
save: async () => {},
|
||||
},
|
||||
gitea: {},
|
||||
diagnostics: null,
|
||||
appInfo: { version: "0.5.2", packaged: false },
|
||||
sourcePath: source,
|
||||
userDataPath: temp,
|
||||
platform: 'win32',
|
||||
platform: "win32",
|
||||
spawnProcess,
|
||||
powershellPath: 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe',
|
||||
powershellPath:
|
||||
"C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
|
||||
handshakeTimeoutMs: 1000,
|
||||
handshakePollMs: 10
|
||||
handshakePollMs: 10,
|
||||
});
|
||||
service.staged = { archivePath: archive, remoteVersion: '0.5.3', remoteSha: 'a'.repeat(40), sha256: 'b'.repeat(64) };
|
||||
service.staged = {
|
||||
archivePath: archive,
|
||||
remoteVersion: "0.5.3",
|
||||
remoteSha: "a".repeat(40),
|
||||
sha256: "b".repeat(64),
|
||||
};
|
||||
const result = await service.apply();
|
||||
assert.equal(result.confirmed, true);
|
||||
assert.ok(capturedArgs.includes('-StatusPath'));
|
||||
assert.ok(capturedArgs.includes('-UpdateId'));
|
||||
assert.ok(capturedArgs.includes("-StatusPath"));
|
||||
assert.ok(capturedArgs.includes("-UpdateId"));
|
||||
await rm(temp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('source updater leaves ForgeFlow open when no STARTED marker arrives', async () => {
|
||||
const temp = await mkdtemp(path.join(os.tmpdir(), 'forgeflow-update-timeout-'));
|
||||
const statusPath = path.join(temp, 'status.json');
|
||||
await writeFile(statusPath, JSON.stringify({ state: 'launching' }));
|
||||
test("source updater leaves ForgeFlow open when no STARTED marker arrives", async () => {
|
||||
const temp = await mkdtemp(
|
||||
path.join(os.tmpdir(), "forgeflow-update-timeout-"),
|
||||
);
|
||||
const statusPath = path.join(temp, "status.json");
|
||||
await writeFile(statusPath, JSON.stringify({ state: "launching" }));
|
||||
await assert.rejects(
|
||||
() => waitForUpdaterStarted(statusPath, { timeoutMs: 80, pollMs: 10, childState: { exited: false, error: null } }),
|
||||
(error) => error.code === 'UPDATE_HELPER_START_TIMEOUT'
|
||||
() =>
|
||||
waitForUpdaterStarted(statusPath, {
|
||||
timeoutMs: 80,
|
||||
pollMs: 10,
|
||||
childState: { exited: false, error: null },
|
||||
}),
|
||||
(error) => error.code === "UPDATE_HELPER_START_TIMEOUT",
|
||||
);
|
||||
await rm(temp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('completed source update result is returned once and acknowledged', async () => {
|
||||
const temp = await mkdtemp(path.join(os.tmpdir(), 'forgeflow-update-result-'));
|
||||
const updates = path.join(temp, 'updates');
|
||||
test("completed source update result is returned once and acknowledged", async () => {
|
||||
const temp = await mkdtemp(
|
||||
path.join(os.tmpdir(), "forgeflow-update-result-"),
|
||||
);
|
||||
const updates = path.join(temp, "updates");
|
||||
await mkdir(updates, { recursive: true });
|
||||
const statusPath = path.join(updates, 'apply-test.status.json');
|
||||
await writeFile(statusPath, JSON.stringify({
|
||||
state: 'success', expectedVersion: '0.5.3', installedVersion: '0.5.3', restartLaunched: false,
|
||||
message: 'installed', logPath: 'C:\\log.txt', updatedAt: new Date().toISOString()
|
||||
}));
|
||||
const statusPath = path.join(updates, "apply-test.status.json");
|
||||
await writeFile(
|
||||
statusPath,
|
||||
JSON.stringify({
|
||||
state: "success",
|
||||
expectedVersion: "0.5.3",
|
||||
installedVersion: "0.5.3",
|
||||
restartLaunched: false,
|
||||
message: "installed",
|
||||
logPath: "C:\\log.txt",
|
||||
updatedAt: new Date().toISOString(),
|
||||
}),
|
||||
);
|
||||
const service = new UpdateService({
|
||||
store: { data: { updates: {} }, save: async () => {} }, gitea: {}, diagnostics: null,
|
||||
appInfo: { version: '0.5.3', packaged: false }, sourcePath: temp, userDataPath: temp
|
||||
store: { data: { updates: {} }, save: async () => {} },
|
||||
gitea: {},
|
||||
diagnostics: null,
|
||||
appInfo: { version: "0.5.3", packaged: false },
|
||||
sourcePath: temp,
|
||||
userDataPath: temp,
|
||||
});
|
||||
const first = await service.consumeLatestResult();
|
||||
const second = await service.consumeLatestResult();
|
||||
assert.equal(first.state, 'success');
|
||||
assert.equal(first.state, "success");
|
||||
assert.equal(first.restartLaunched, false);
|
||||
assert.equal(second, null);
|
||||
const persisted = JSON.parse(await readFile(statusPath, 'utf8'));
|
||||
const persisted = JSON.parse(await readFile(statusPath, "utf8"));
|
||||
assert.ok(persisted.acknowledgedAt);
|
||||
await rm(temp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('PowerShell update helper writes lifecycle status before waiting for ForgeFlow exit', async () => {
|
||||
const script = await readFile(new URL('../scripts/apply-source-update.ps1', import.meta.url), 'utf8');
|
||||
test("PowerShell update helper writes lifecycle status before waiting for ForgeFlow exit", async () => {
|
||||
const script = await readFile(
|
||||
new URL("../scripts/apply-source-update.ps1", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
assert.match(script, /\[string\]\$StatusPath/);
|
||||
assert.match(script, /Write-UpdateState -State "started"/);
|
||||
assert.match(script, /Write-UpdateState -State "success"/);
|
||||
@@ -141,23 +215,29 @@ test('PowerShell update helper writes lifecycle status before waiting for ForgeF
|
||||
assert.match(script, /WriteAllText/);
|
||||
});
|
||||
|
||||
|
||||
test('PowerShell update helper starts with param and has no BOM or stray leading slash', async () => {
|
||||
const bytes = await readFile(new URL('../scripts/apply-source-update.ps1', import.meta.url));
|
||||
assert.notDeepEqual([...bytes.subarray(0, 3)], [0xEF, 0xBB, 0xBF]);
|
||||
const text = bytes.toString('utf8');
|
||||
test("PowerShell update helper starts with param and has no BOM or stray leading slash", async () => {
|
||||
const bytes = await readFile(
|
||||
new URL("../scripts/apply-source-update.ps1", import.meta.url),
|
||||
);
|
||||
assert.notDeepEqual([...bytes.subarray(0, 3)], [0xef, 0xbb, 0xbf]);
|
||||
const text = bytes.toString("utf8");
|
||||
assert.match(text.trimStart(), /^param\(/);
|
||||
assert.doesNotMatch(text.trimStart(), /^\\/);
|
||||
assert.match(text, /node_modules\\electron\\dist\\electron\.exe/);
|
||||
assert.match(text, /npm ci --no-audit --no-fund/);
|
||||
assert.match(text, /package-lock\.json/);
|
||||
assert.doesNotMatch(text, /Get-Command npm\.cmd/);
|
||||
assert.ok(text.indexOf('Write-UpdateState -State "success"') < text.indexOf('Start-ForgeFlow -WorkingDirectory $SourcePath'));
|
||||
|
||||
assert.ok(
|
||||
text.indexOf('Write-UpdateState -State "success"') <
|
||||
text.indexOf("Start-ForgeFlow -WorkingDirectory $SourcePath"),
|
||||
);
|
||||
});
|
||||
|
||||
test('release publisher verifies Gitea and bootstraps the installed updater service and helper', async () => {
|
||||
const script = await readFile(new URL('../Publish-ForgeFlow-Release.ps1', import.meta.url), 'utf8');
|
||||
test("release publisher verifies Gitea and bootstraps the installed updater service and helper", async () => {
|
||||
const script = await readFile(
|
||||
new URL("../Publish-ForgeFlow-Release.ps1", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
assert.match(script, /npm install --no-audit --no-fund/);
|
||||
assert.match(script, /package-lock\.json/);
|
||||
assert.match(script, /non-reproducible update/);
|
||||
@@ -175,47 +255,205 @@ test('release publisher verifies Gitea and bootstraps the installed updater serv
|
||||
assert.doesNotMatch(script, /Copy-Item[^\n]+package\.json/);
|
||||
});
|
||||
|
||||
|
||||
test('PowerShell helper replaces an existing launching status with a Windows-safe file API', async () => {
|
||||
const script = await readFile(new URL('../scripts/apply-source-update.ps1', import.meta.url), 'utf8');
|
||||
assert.match(script, /System\.IO\.File\]::Replace\(\$temporary, \$StatusPath, \$null\)/);
|
||||
assert.match(script, /System\.IO\.File\]::Copy\(\$temporary, \$StatusPath, \$true\)/);
|
||||
assert.doesNotMatch(script, /Move-Item -LiteralPath \$temporary -Destination \$StatusPath -Force/);
|
||||
test("PowerShell helper replaces an existing launching status with a Windows-safe file API", async () => {
|
||||
const script = await readFile(
|
||||
new URL("../scripts/apply-source-update.ps1", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
assert.match(
|
||||
script,
|
||||
/System\.IO\.File\]::Replace\(\$temporary, \$StatusPath, \$null\)/,
|
||||
);
|
||||
assert.match(
|
||||
script,
|
||||
/System\.IO\.File\]::Copy\(\$temporary, \$StatusPath, \$true\)/,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
script,
|
||||
/Move-Item -LiteralPath \$temporary -Destination \$StatusPath -Force/,
|
||||
);
|
||||
assert.match(script, /\[switch\]\$HandshakeOnly/);
|
||||
assert.match(script, /Handshake-only verification completed successfully/);
|
||||
});
|
||||
|
||||
test('early helper exit reports the helper log instead of only an exit code', async () => {
|
||||
const temp = await mkdtemp(path.join(os.tmpdir(), 'forgeflow-update-log-tail-'));
|
||||
const statusPath = path.join(temp, 'status.json');
|
||||
const logPath = path.join(temp, 'apply.log');
|
||||
await writeFile(statusPath, JSON.stringify({ state: 'launching', updateId: 'request-1' }));
|
||||
await writeFile(logPath, 'first line\nactual helper failure\n');
|
||||
test("early helper exit reports the helper log instead of only an exit code", async () => {
|
||||
const temp = await mkdtemp(
|
||||
path.join(os.tmpdir(), "forgeflow-update-log-tail-"),
|
||||
);
|
||||
const statusPath = path.join(temp, "status.json");
|
||||
const logPath = path.join(temp, "apply.log");
|
||||
await writeFile(
|
||||
statusPath,
|
||||
JSON.stringify({ state: "launching", updateId: "request-1" }),
|
||||
);
|
||||
await writeFile(logPath, "first line\nactual helper failure\n");
|
||||
await assert.rejects(
|
||||
() => waitForUpdaterStarted(statusPath, {
|
||||
timeoutMs: 100,
|
||||
pollMs: 5,
|
||||
childState: { exited: true, code: 0, error: null },
|
||||
expectedUpdateId: 'request-1',
|
||||
logPath
|
||||
}),
|
||||
(error) => error.code === 'UPDATE_HELPER_EXITED_EARLY' && /actual helper failure/.test(error.message)
|
||||
() =>
|
||||
waitForUpdaterStarted(statusPath, {
|
||||
timeoutMs: 100,
|
||||
pollMs: 5,
|
||||
childState: { exited: true, code: 0, error: null },
|
||||
expectedUpdateId: "request-1",
|
||||
logPath,
|
||||
}),
|
||||
(error) =>
|
||||
error.code === "UPDATE_HELPER_EXITED_EARLY" &&
|
||||
/actual helper failure/.test(error.message),
|
||||
);
|
||||
await rm(temp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('updater handshake rejects a stale status from another update request', async () => {
|
||||
const temp = await mkdtemp(path.join(os.tmpdir(), 'forgeflow-update-id-'));
|
||||
const statusPath = path.join(temp, 'status.json');
|
||||
await writeFile(statusPath, JSON.stringify({ state: 'started', updateId: 'old-request' }));
|
||||
test("updater handshake rejects a stale status from another update request", async () => {
|
||||
const temp = await mkdtemp(path.join(os.tmpdir(), "forgeflow-update-id-"));
|
||||
const statusPath = path.join(temp, "status.json");
|
||||
await writeFile(
|
||||
statusPath,
|
||||
JSON.stringify({ state: "started", updateId: "old-request" }),
|
||||
);
|
||||
await assert.rejects(
|
||||
() => waitForUpdaterStarted(statusPath, {
|
||||
timeoutMs: 50,
|
||||
pollMs: 5,
|
||||
childState: { exited: false, code: null, error: null },
|
||||
expectedUpdateId: 'new-request'
|
||||
}),
|
||||
(error) => error.code === 'UPDATE_HELPER_START_TIMEOUT'
|
||||
() =>
|
||||
waitForUpdaterStarted(statusPath, {
|
||||
timeoutMs: 50,
|
||||
pollMs: 5,
|
||||
childState: { exited: false, code: null, error: null },
|
||||
expectedUpdateId: "new-request",
|
||||
}),
|
||||
(error) => error.code === "UPDATE_HELPER_START_TIMEOUT",
|
||||
);
|
||||
await rm(temp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("packaged updater downloads only a published checksum-matched Windows asset", async () => {
|
||||
const temp = await mkdtemp(
|
||||
path.join(os.tmpdir(), "forgeflow-binary-update-"),
|
||||
);
|
||||
const binary = Buffer.alloc(1_100_000, 0x5a);
|
||||
binary[0] = 0x4d;
|
||||
binary[1] = 0x5a;
|
||||
const sha256 = createHash("sha256").update(binary).digest("hex");
|
||||
const assetName = "ForgeFlow-Setup-0.8.2-win-x64.exe";
|
||||
const gitea = {
|
||||
async getReleaseByTag(_owner, _repo, tag) {
|
||||
if (tag !== "v0.8.2") return null;
|
||||
return {
|
||||
tag_name: tag,
|
||||
draft: false,
|
||||
prerelease: false,
|
||||
assets: [
|
||||
{ name: assetName, browser_download_url: "https://gitea.test/setup" },
|
||||
{
|
||||
name: `${assetName}.sha256`,
|
||||
browser_download_url: "https://gitea.test/checksum",
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
async downloadAuthenticated(url) {
|
||||
return url.endsWith("/checksum")
|
||||
? Buffer.from(`${sha256} ${assetName}\n`)
|
||||
: binary;
|
||||
},
|
||||
};
|
||||
const service = new UpdateService({
|
||||
store: {
|
||||
data: { gitea: { baseUrl: "https://gitea.test" } },
|
||||
save: async () => {},
|
||||
},
|
||||
gitea,
|
||||
diagnostics: null,
|
||||
appInfo: {
|
||||
version: "0.8.1",
|
||||
packaged: true,
|
||||
executablePath: "C:\\ForgeFlow\\ForgeFlow.exe",
|
||||
},
|
||||
sourcePath: temp,
|
||||
userDataPath: temp,
|
||||
platform: "win32",
|
||||
});
|
||||
const result = await service.downloadPackaged({
|
||||
owner: "Jens",
|
||||
repo: "ForgeFlow",
|
||||
remoteVersion: "0.8.2",
|
||||
});
|
||||
assert.equal(result.downloaded, true);
|
||||
assert.equal(result.sha256, sha256);
|
||||
assert.equal(result.portable, false);
|
||||
assert.equal((await readFile(result.binaryPath)).length, binary.length);
|
||||
await rm(temp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("packaged updater rejects a binary whose checksum does not match", async () => {
|
||||
const temp = await mkdtemp(
|
||||
path.join(os.tmpdir(), "forgeflow-binary-mismatch-"),
|
||||
);
|
||||
const binary = Buffer.alloc(1_100_000, 0x5a);
|
||||
binary[0] = 0x4d;
|
||||
binary[1] = 0x5a;
|
||||
const assetName = "ForgeFlow-Portable-0.8.2-win-x64.exe";
|
||||
const service = new UpdateService({
|
||||
store: { data: { gitea: {} }, save: async () => {} },
|
||||
gitea: {
|
||||
async getReleaseByTag() {
|
||||
return {
|
||||
tag_name: "v0.8.2",
|
||||
assets: [
|
||||
{
|
||||
name: assetName,
|
||||
browser_download_url: "https://gitea.test/portable",
|
||||
},
|
||||
{
|
||||
name: `${assetName}.sha256`,
|
||||
browser_download_url: "https://gitea.test/checksum",
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
async downloadAuthenticated(url) {
|
||||
return url.endsWith("/checksum")
|
||||
? Buffer.from(`${"0".repeat(64)} ${assetName}`)
|
||||
: binary;
|
||||
},
|
||||
},
|
||||
diagnostics: null,
|
||||
appInfo: {
|
||||
version: "0.8.1",
|
||||
packaged: true,
|
||||
portableExecutablePath: "C:\\ForgeFlow-Portable.exe",
|
||||
},
|
||||
sourcePath: temp,
|
||||
userDataPath: temp,
|
||||
platform: "win32",
|
||||
});
|
||||
await assert.rejects(
|
||||
() =>
|
||||
service.downloadPackaged({
|
||||
owner: "Jens",
|
||||
repo: "ForgeFlow",
|
||||
remoteVersion: "0.8.2",
|
||||
}),
|
||||
/SHA-256 verification/,
|
||||
);
|
||||
await rm(temp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("binary update helper verifies, waits, applies and records restart state", async () => {
|
||||
const helper = await readFile(
|
||||
new URL("../scripts/apply-binary-update.ps1", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
for (const marker of [
|
||||
"Get-FileHash",
|
||||
"Wait-Process",
|
||||
'Write-UpdateState -State "started"',
|
||||
'Write-UpdateState -State "waiting-for-exit"',
|
||||
'Write-UpdateState -State "applying"',
|
||||
'Write-UpdateState -State "success"',
|
||||
'Start-Process -FilePath $BinaryPath -ArgumentList "/S"',
|
||||
"Copy-Item -LiteralPath $BinaryPath -Destination $CurrentExecutable",
|
||||
]) {
|
||||
assert.ok(
|
||||
helper.includes(marker),
|
||||
`missing binary updater marker: ${marker}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user