Release ForgeFlow 0.5.2

This commit is contained in:
NuklearRabbit
2026-07-25 01:07:54 +02:00
parent 602309b203
commit cf1f67a823
23 changed files with 550 additions and 72 deletions
+26
View File
@@ -152,3 +152,29 @@ test('keeps a successful local commit visible as ahead when the following push f
const subject = await git(['log', '-1', '--pretty=%s'], working);
assert.equal(subject.stdout.trim(), 'Update portfolio');
});
test('stages a large Windows-sized partial selection through NUL-delimited stdin', async (t) => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'forgeflow-git-large-selection-'));
t.after(() => fs.rm(root, { recursive: true, force: true }));
const working = path.join(root, 'working');
await fs.mkdir(working);
await git(['init'], working);
await git(['config', 'user.name', 'ForgeFlow Test'], working);
await git(['config', 'user.email', 'forgeflow@example.invalid'], working);
await fs.writeFile(path.join(working, 'README.md'), '# Large selection\n');
await git(['add', '.'], working);
await git(['commit', '-m', 'Initial'], working);
const names = [];
for (let index = 0; index < 850; index += 1) {
const name = `generated/feature-${String(index).padStart(4, '0')}-${'x'.repeat(28)}.txt`;
names.push(name);
await fs.mkdir(path.dirname(path.join(working, name)), { recursive: true });
await fs.writeFile(path.join(working, name), `file ${index}\n`);
}
const service = new GitService();
const status = await service.stage(working, names);
assert.equal(status.counts.staged, names.length);
assert.equal(status.counts.unstaged, 0);
});
+32
View File
@@ -22,3 +22,35 @@ test('ITWorx branding is integrated into titlebar and setup', async () => {
assert.match(renderer, /itworx-mark\.png/);
assert.match(renderer, /itworx-wordmark\.png/);
});
test('all modal content stays inside the viewport with a persistent action footer', async () => {
const css = await readFile(new URL('../src/renderer/styles.css', import.meta.url), 'utf8');
assert.match(css, /\.modal\s*\{[^}]*max-height:\s*calc\(100dvh[^}]*display:\s*flex[^}]*flex-direction:\s*column/);
assert.match(css, /\.modal-body\s*\{[^}]*min-height:\s*0[^}]*overflow-y:\s*auto/);
assert.match(css, /\.modal-footer\s*\{[^}]*flex:\s*0 0 auto/);
});
test('settings provides one-click normalization for legacy Gitea origins', async () => {
const renderer = await readFile(new URL('../src/renderer/app.js', import.meta.url), 'utf8');
assert.match(renderer, /data-action="normalize-origins"/);
assert.match(renderer, /Normalize all origins/);
});
test('Git mutations are serialized per repository and expose repair actions', async () => {
const ipc = await readFile(new URL('../src/main/ipc.cjs', import.meta.url), 'utf8');
const renderer = await readFile(new URL('../src/renderer/app.js', import.meta.url), 'utf8');
assert.match(ipc, /repositoryMutations = new Map/);
assert.match(ipc, /withRepositoryMutation/);
assert.match(renderer, /data-action="repair-index-lock"/);
assert.match(renderer, /data-action="repair-origin"/);
});
test('SSH secrets are captured before the loading render clears password inputs', async () => {
const renderer = await readFile(new URL('../src/renderer/app.js', import.meta.url), 'utf8');
const passwordCapture = renderer.indexOf("const password = document.querySelector('#server-password')");
const loading = renderer.indexOf("setLoading(true, 'Saving encrypted SSH configuration…')");
assert.ok(passwordCapture >= 0 && loading > passwordCapture);
});
+55 -1
View File
@@ -6,7 +6,7 @@ import { spawnSync } from 'node:child_process';
import test from 'node:test';
import shellVerification from '../src/shared/shell-verification.cjs';
const { bashSyntaxCheckInvocation, normalizeRelativePosixPath } = shellVerification;
const { bashSyntaxCheckInvocation, bashSyntaxCheckFromTextInvocation, normalizeRelativePosixPath, validateShellScriptStructure, shouldRunExternalBash } = shellVerification;
test('Bash syntax validation keeps Windows project roots in cwd and passes a relative POSIX path', () => {
const invocation = bashSyntaxCheckInvocation('C:\\Projects\\ForgeFlow');
@@ -47,3 +47,57 @@ test('Bash syntax validation works from a project root containing spaces', async
}
}
});
test('Bash syntax validation from text does not depend on a Windows working directory', () => {
const invocation = bashSyntaxCheckFromTextInvocation('#!/usr/bin/env bash\nset -euo pipefail\necho ok\n');
assert.equal(invocation.command, 'bash');
assert.deepEqual(invocation.args, ['-n']);
assert.equal(invocation.options.cwd, undefined);
assert.match(invocation.options.input, /set -euo pipefail/);
});
test('Bash syntax validation from text detects malformed scripts', (t) => {
if (spawnSync('bash', ['--version'], { encoding: 'utf8' }).status !== 0) {
t.skip('Bash is not available in this environment.');
return;
}
const invocation = bashSyntaxCheckFromTextInvocation('if true; then\n echo missing fi\n');
const result = spawnSync(invocation.command, invocation.args, invocation.options);
assert.notEqual(result.status, 0);
});
test('portable server-script validation does not require a local Bash executable', () => {
const script = `#!/usr/bin/env bash
set -Eeuo pipefail
readonly CONFIG_FILE="/etc/forgeflow/targets.conf"
echo "Target configuration must be owned by root"
APP_DIR=/tmp/app
SHA=0123456789012345678901234567890123456789
COMPOSE_FILE=docker-compose.yml
write_status() { :; }
exec 9>/tmp/test.lock
flock -n 9
git -C "$APP_DIR" fetch origin main
git -C "$APP_DIR" reset --hard "$SHA"
docker compose -f "$COMPOSE_FILE" up -d --build --remove-orphans
write_status "healthy"
write_status "unhealthy"
`;
assert.equal(validateShellScriptStructure(script), true);
});
test('portable server-script validation refuses missing deployment safety markers', () => {
assert.throws(
() => validateShellScriptStructure('#!/usr/bin/env bash\nset -Eeuo pipefail\necho unsafe\n'),
/missing required safety marker/
);
});
test('Windows publication never depends on an external Bash shim', () => {
assert.equal(shouldRunExternalBash('win32'), false);
assert.equal(shouldRunExternalBash('linux'), true);
assert.equal(shouldRunExternalBash('darwin'), true);
});
+8
View File
@@ -1,8 +1,10 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import toolInvocation from '../src/shared/tool-invocation.cjs';
import processRunner from '../src/main/process-runner.cjs';
const { npmProbeCandidates } = toolInvocation;
const { run } = processRunner;
test('uses npm CLI through Node when doctor is launched by npm on Windows', () => {
const candidates = npmProbeCandidates({
@@ -39,3 +41,9 @@ test('uses npm directly on non-Windows systems', () => {
{ file: 'npm', args: ['--version'], source: 'path' }
]);
});
test('process runner accepts stdin for Git pathspec transport', async () => {
const result = await run(process.execPath, ['-e', 'process.stdin.pipe(process.stdout)'], { input: 'a\0b\0' });
assert.equal(result.stdout, 'a\0b\0');
});