Release ForgeFlow 0.5.2
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user