feat: add safe Gitea sync and signed updates
ForgeFlow quality gate / secret-scan (push) Failing after 32s
ForgeFlow quality gate / quality (push) Failing after 0s

This commit is contained in:
NuklearRabbit
2026-08-27 00:38:58 +02:00
parent cb9bdcd713
commit d47c7b5e41
46 changed files with 1658 additions and 246 deletions
+104
View File
@@ -53,6 +53,35 @@ test('GitService reads changes and commits/pushes selected files to a real bare
assert.equal(remoteLog.stdout.trim(), 'Add desktop cockpit copy');
});
test('untracked diff rendering refuses links outside the repository and oversized files', async (t) => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'forgeflow-diff-boundary-'));
t.after(() => fs.rm(root, { recursive: true, force: true }));
const repository = path.join(root, 'repository');
const outside = path.join(root, 'outside');
await fs.mkdir(repository, { recursive: true });
await fs.mkdir(outside, { recursive: true });
await git(['init'], repository);
await fs.writeFile(path.join(outside, 'secret.txt'), 'outside-secret');
try {
await fs.symlink(outside, path.join(repository, 'linked'), process.platform === 'win32' ? 'junction' : 'dir');
} catch {
t.skip('this platform does not allow creating directory links');
return;
}
const service = new GitService();
await assert.rejects(
service.diff(repository, 'linked/secret.txt'),
(error) => error.code === 'DIFF_TARGET_OUTSIDE_REPOSITORY',
);
await fs.writeFile(path.join(repository, 'too-large.txt'), Buffer.alloc(16 * 1024 * 1024 + 1, 0x61));
await assert.rejects(
service.diff(repository, 'too-large.txt'),
(error) => error.code === 'DIFF_FILE_TOO_LARGE' && error.recoverable === true,
);
});
test('stages and pushes deleted and renamed files selected from the working tree', async (t) => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'forgeflow-git-delete-rename-'));
t.after(() => fs.rm(root, { recursive: true, force: true }));
@@ -204,6 +233,81 @@ test('detects and removes a stale HEAD.lock while skipping Git object storage',
assert.ok(await fs.stat(ignoredObjectLock));
});
test('previews and safely mirrors a workspace to Gitea while preserving every class of local work', async (t) => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'forgeflow-workspace-sync-'));
t.after(() => fs.rm(root, { recursive: true, force: true }));
const remote = path.join(root, 'remote.git');
const working = path.join(root, 'working');
const external = path.join(root, 'external');
await git(['init', '--bare', remote], root);
await git(['clone', remote, working], root);
await git(['config', 'user.name', 'ForgeFlow Test'], working);
await git(['config', 'user.email', 'forgeflow@example.invalid'], working);
await fs.writeFile(path.join(working, '.gitignore'), 'runtime/\n');
await fs.writeFile(path.join(working, 'README.md'), 'initial\n');
await fs.writeFile(path.join(working, 'obsolete.txt'), 'remove remotely\n');
await git(['add', '.'], working);
await git(['commit', '-m', 'Initial'], working);
await git(['branch', '-M', 'main'], working);
await git(['push', '-u', 'origin', 'main'], working);
await git(['clone', remote, external], root);
await git(['config', 'user.name', 'External Gitea Test'], external);
await git(['config', 'user.email', 'external@example.invalid'], external);
await git(['checkout', 'main'], external);
await fs.writeFile(path.join(external, 'README.md'), 'changed on Gitea\n');
await fs.rm(path.join(external, 'obsolete.txt'));
await fs.writeFile(path.join(external, 'remote-only.txt'), 'new on Gitea\n');
await git(['add', '-A'], external);
await git(['commit', '-m', 'External cleanup'], external);
await git(['push', 'origin', 'main'], external);
await fs.writeFile(path.join(working, 'local-commit.txt'), 'local committed work\n');
await git(['add', 'local-commit.txt'], working);
await git(['commit', '-m', 'Local Codex work'], working);
const localHead = (await git(['rev-parse', 'HEAD'], working)).stdout.trim();
await fs.appendFile(path.join(working, 'README.md'), 'local uncommitted edit\n');
await fs.writeFile(path.join(working, 'local-notes.txt'), 'untracked local notes\n');
await fs.mkdir(path.join(working, 'runtime'), { recursive: true });
await fs.writeFile(path.join(working, 'runtime', 'local.db'), 'ignored runtime state\n');
const service = new GitService();
const firstPlan = await service.previewWorkspaceSync(working);
assert.match(firstPlan.id, /^[0-9a-f]{64}$/);
assert.equal(firstPlan.summary.localCommitsToProtect, 1);
assert.equal(firstPlan.summary.incomingCommits, 1);
assert.equal(firstPlan.summary.localFilesToStash, 2);
assert.equal(firstPlan.summary.untrackedFilesToStash, 1);
assert.ok(firstPlan.changes.some((item) => item.path === 'obsolete.txt' && item.code === 'D'));
assert.equal(firstPlan.recovery.ignoredFilesPreserved, true);
await fs.writeFile(path.join(working, 'changed-after-preview.txt'), 'forces a stale plan\n');
await assert.rejects(
service.synchronizeWorkspace(working, firstPlan.id),
(error) => error.code === 'WORKSPACE_SYNC_PLAN_STALE'
);
assert.equal(await fs.readFile(path.join(working, 'changed-after-preview.txt'), 'utf8'), 'forces a stale plan\n');
const reviewedPlan = await service.previewWorkspaceSync(working);
const result = await service.synchronizeWorkspace(working, reviewedPlan.id);
assert.equal(result.applied, true);
assert.equal(result.status.clean, true);
assert.equal(result.status.head, reviewedPlan.targetSha);
assert.match(result.backupBranch, /^forgeflow\/recovery-main-/);
assert.ok(result.stash?.sha);
assert.equal((await git(['rev-parse', result.backupBranch], working)).stdout.trim(), localHead);
assert.equal((await fs.readFile(path.join(working, 'README.md'), 'utf8')).replace(/\r\n/g, '\n'), 'changed on Gitea\n');
assert.equal((await fs.readFile(path.join(working, 'remote-only.txt'), 'utf8')).replace(/\r\n/g, '\n'), 'new on Gitea\n');
await assert.rejects(fs.stat(path.join(working, 'obsolete.txt')), (error) => error.code === 'ENOENT');
await assert.rejects(fs.stat(path.join(working, 'local-commit.txt')), (error) => error.code === 'ENOENT');
await assert.rejects(fs.stat(path.join(working, 'local-notes.txt')), (error) => error.code === 'ENOENT');
assert.equal(await fs.readFile(path.join(working, 'runtime', 'local.db'), 'utf8'), 'ignored runtime state\n');
const stashedPaths = (await git(['stash', 'show', '--include-untracked', '--name-only', result.stash.ref], working)).stdout;
assert.match(stashedPaths, /README\.md/);
assert.match(stashedPaths, /local-notes\.txt/);
assert.match(stashedPaths, /changed-after-preview\.txt/);
});
test('repairs a diverged branch by creating a safety branch before resetting to upstream', async (t) => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'forgeflow-diverged-'));
t.after(() => fs.rm(root, { recursive: true, force: true }));