test: cover the deployment and deploy-key paths, and gate coverage per module

deployment-service.cjs sat at 52% and unraid-deploy-key-host.cjs at 39% of its
functions, both hidden behind a healthy aggregate. They are now at 100% lines
and functions, tested through real HTTP endpoints and by intercepting the shell
script the key host sends, rather than by mocking the boundary away.

What is pinned down: a successful workflow run still fails when the server
cannot prove it runs that exact commit; a rollback ends as rolled-back rather
than success; an unreachable status endpoint is never treated as healthy; a
failed poll is recorded on the operation instead of losing it; deploy keys stay
repository-scoped under the server base path with a pinned host key; promotion
verifies the candidate before swapping atomically; and revocation moves key
material to recovery instead of deleting it.

Two assumptions turned out to be wrong and the tests follow the real behaviour:
the previous-SHA check runs before the already-live check, and a rollback
against an unreachable endpoint surfaces the underlying network error.

Covering clone-target exposed a real defect: a remote ending in "....git"
yielded the folder name "...". Windows strips trailing dots, so that resolves
back to the project root itself, past an escape guard that only looks for "..".
A dots-only name now falls back to "repository", consistent with how an empty
name was already handled. As a side effect "." and ".." resolve to a usable
folder instead of raising an error.

Coverage gates: the aggregate moves to 85/85/68, and a new per-module gate
(60 statements, 50 functions, 36 branches) stops a single module from silently
collapsing behind the total. It reuses the data from the first run, so the
suite is not executed twice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
NuklearRabbit
2026-08-23 14:32:12 +02:00
co-authored by Claude Opus 5
parent 9260d35957
commit a5666e95f2
8 changed files with 898 additions and 154 deletions
+29
View File
@@ -26,6 +26,35 @@ test('resolves the automatic clone target inside the configured project root', (
assert.equal(plan.directoryName, 'portfolio');
});
test('a clone target that would leave the project root is refused', () => {
const root = path.join(os.tmpdir(), 'forgeflow-projects');
const resolved = path.resolve(root);
// The escape guard inside resolveCloneTarget stays as a backstop, but no
// sanitised folder name can reach it any more: the name is a single path
// segment and a dots-only segment falls back to "repository".
for (const remote of ['..', '.', '../escape', '/', '', '....git', 'https://gitea.example.test/jens/....git']) {
const plan = resolveCloneTarget(root, remote);
assert.ok(
plan.target.startsWith(`${resolved}${path.sep}`) && plan.target !== resolved,
`${remote} resolved outside the project root: ${plan.target}`,
);
}
for (const badRoot of ['', ' ', null, undefined]) {
assert.throws(() => resolveCloneTarget(badRoot, 'https://gitea.example.test/jens/app.git'), /project root is required/);
}
});
test('a folder name that sanitises away still produces a usable directory', () => {
// Windows strips trailing dots, so a dots-only name would land on the project
// root itself instead of a subdirectory.
assert.equal(cloneDirectoryName('https://gitea.example.test/jens/....git'), 'repository');
assert.equal(cloneDirectoryName('..'), 'repository');
assert.equal(cloneDirectoryName(''), 'repository');
assert.equal(cloneDirectoryName('https://gitea.example.test/jens/app.git#readme'), 'app');
assert.equal(cloneDirectoryName('https://gitea.example.test/jens/spaced name.git'), 'spaced-name');
});
test('clone target inspection accepts missing and empty destinations', async (t) => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'forgeflow-clone-target-'));
t.after(() => fs.rm(root, { recursive: true, force: true }));