import test from 'node:test'; import assert from 'node:assert/strict'; import deploymentModule from '../src/main/deployment-service.cjs'; const { DeploymentService, terminalRunConclusion, applicationVerificationFailure } = deploymentModule; test('maps runner conclusions to ForgeFlow terminal states', () => { assert.equal(terminalRunConclusion({ conclusion: 'success' }), 'success'); assert.equal(terminalRunConclusion({ conclusion: 'failure' }), 'failed'); assert.equal(terminalRunConclusion({ status: 'timed_out' }), 'failed'); assert.equal(terminalRunConclusion({ conclusion: 'cancelled' }), 'cancelled'); assert.equal(terminalRunConclusion({ status: 'running' }), null); }); test('dispatches only controlled deployment inputs', async () => { const sha = 'a'.repeat(40); let dispatched = null; const operations = new Map(); const profile = { id: 'staging', name: 'Staging', environment: 'staging', branch: 'main', workflowFile: 'deploy.yml', rollbackWorkflowFile: 'rollback.yml', statusUrl: 'https://app.example.test/.well-known/forgeflow', inputs: { commit_sha: 'b'.repeat(40), request_id: 'forged', arbitrary: 'ignored' } }; const store = { getDeploymentProfile: () => profile, getToken: () => '', addOperation: async (operation) => { operations.set(operation.id, structuredClone(operation)); return structuredClone(operation); } }; const service = new DeploymentService(store, { dispatchWorkflow: async (payload) => { dispatched = payload; return { accepted: true, status: 204 }; } }, { status: async () => ({ head: sha, clean: true, counts: { changed: 0 }, branch: { head: 'main', upstream: 'origin/main', ahead: 0, behind: 0 } }), verifyCommitOnRemoteBranch: async () => ({ valid: true }) }, { info: async () => {}, error: async () => {} }); const operation = await service.deploy({ repository: { fullName: 'jens/app', localPath: '/repo' }, profileId: profile.id, sha }); assert.deepEqual(Object.keys(dispatched.inputs).sort(), ['commit_sha', 'environment', 'request_id']); assert.equal(dispatched.inputs.commit_sha, sha); assert.equal(dispatched.inputs.environment, 'staging'); assert.equal(dispatched.inputs.request_id, operation.id); assert.equal(dispatched.inputs.arbitrary, undefined); }); test('requires exact server SHA and matching request ID after a successful workflow', () => { const operation = { id: 'request-1', repository: 'jens/app', environment: 'staging', sha: 'a'.repeat(40), shortSha: 'aaaaaaa' }; assert.equal(applicationVerificationFailure(operation, { statusConfigured: true, statusReachable: true, statusRepository: 'jens/app', statusEnvironment: 'staging', liveSha: operation.sha, requestedSha: operation.sha, requestId: operation.id, lastExitCode: 0, healthy: true }), null); assert.match(applicationVerificationFailure(operation, { statusConfigured: true, statusReachable: true, statusRepository: 'jens/app', statusEnvironment: 'staging', liveSha: operation.sha, requestedSha: operation.sha, requestId: 'another-request', lastExitCode: 0, healthy: true }).message, /different deployment request/i); assert.match(applicationVerificationFailure(operation, { statusConfigured: true, statusReachable: true, statusRepository: 'jens/app', statusEnvironment: 'staging', liveSha: 'b'.repeat(40), requestedSha: operation.sha, requestId: operation.id, lastExitCode: 0, healthy: true }).message, /server reports/i); assert.match(applicationVerificationFailure(operation, { statusConfigured: true, statusReachable: false, liveSha: null, requestId: null, healthy: null }).message, /not reachable/i); assert.match(applicationVerificationFailure(operation, { statusConfigured: true, statusReachable: true, statusRepository: 'other/app', statusEnvironment: 'staging', liveSha: operation.sha, requestedSha: operation.sha, requestId: operation.id, lastExitCode: 0, healthy: true }).message, /belongs to other\/app/i); assert.match(applicationVerificationFailure(operation, { statusConfigured: true, statusReachable: true, statusRepository: 'jens/app', statusEnvironment: 'staging', liveSha: operation.sha, requestedSha: operation.sha, requestId: operation.id, lastExitCode: 70, healthy: false }).message, /exit code 70/i); }); test('server verification reports every incomplete or mismatched evidence field', () => { const sha = 'a'.repeat(40); const operation = { id: 'request-1', repository: 'jens/app', environment: 'production', sha, shortSha: sha.slice(0, 7) }; const valid = { statusConfigured: true, statusReachable: true, statusRepository: operation.repository, statusEnvironment: operation.environment, liveSha: sha, requestedSha: sha, requestId: operation.id, lastExitCode: 0, healthy: true }; const cases = [ [{ ...valid, statusConfigured: false }, /is configured/i], [{ ...valid, statusRepository: '' }, /did not identify its repository/i], [{ ...valid, statusEnvironment: '' }, /did not identify its environment/i], [{ ...valid, statusEnvironment: 'staging' }, /belongs to staging/i], [{ ...valid, liveSha: '' }, /valid full commit SHA/i], [{ ...valid, requestedSha: '' }, /requested commit SHA/i], [{ ...valid, requestedSha: 'b'.repeat(40) }, /different requested commit/i], [{ ...valid, requestId: '' }, /deployment request ID/i], [{ ...valid, lastExitCode: null }, /exit code unknown/i], [{ ...valid, healthy: false, healthStatus: 'degraded' }, /degraded/i], [{ ...valid, healthy: null, error: 'probe failed' }, /probe failed/i] ]; for (const [state, pattern] of cases) assert.match(applicationVerificationFailure(operation, state).message, pattern); }); test('rollback accepts only the currently reported previous SHA and dispatches controlled inputs', async () => { const liveSha = 'a'.repeat(40); const previousSha = 'b'.repeat(40); let dispatched = null; let verified = null; const operations = new Map(); const profile = { id: 'production', name: 'Production', environment: 'production', branch: 'main', workflowFile: 'deploy.yml', rollbackWorkflowFile: 'rollback.yml', statusUrl: 'https://app.example.test/.well-known/forgeflow', inputs: { target_sha: 'c'.repeat(40), request_id: 'forged', arbitrary: 'ignored' } }; const store = { getDeploymentProfile: () => profile, getToken: () => '', addOperation: async (operation) => { operations.set(operation.id, structuredClone(operation)); return structuredClone(operation); } }; const service = new DeploymentService(store, { listWorkflowRuns: async () => ({ runs: [] }), dispatchWorkflow: async (payload) => { dispatched = payload; return { accepted: true, status: 204 }; } }, { verifyCommitOnRemoteBranch: async (...args) => { verified = args; return { valid: true }; } }, { info: async () => {}, warning: async () => {}, error: async () => {} }); service.refreshProfileState = async () => ({ statusReachable: true, statusRepository: 'jens/app', statusEnvironment: 'production', liveSha, previousSha }); const operation = await service.rollback({ repository: { fullName: 'jens/app', localPath: '/repo' }, profileId: profile.id, targetSha: previousSha }); assert.deepEqual(verified, ['/repo', previousSha, 'main']); assert.deepEqual(Object.keys(dispatched.inputs).sort(), ['environment', 'request_id', 'target_sha']); assert.equal(dispatched.inputs.environment, 'production'); assert.equal(dispatched.inputs.target_sha, previousSha); assert.equal(dispatched.inputs.request_id, operation.id); assert.equal(dispatched.inputs.arbitrary, undefined); }); test('rollback refuses a stale target that is no longer the server-reported previous SHA', async () => { const previousSha = 'b'.repeat(40); let dispatched = false; const profile = { id: 'production', name: 'Production', environment: 'production', branch: 'main', workflowFile: 'deploy.yml', rollbackWorkflowFile: 'rollback.yml', statusUrl: 'https://app.example.test/.well-known/forgeflow' }; const service = new DeploymentService({ getDeploymentProfile: () => profile, getToken: () => '', addOperation: async (operation) => operation }, { dispatchWorkflow: async () => { dispatched = true; } }, { verifyCommitOnRemoteBranch: async () => ({ valid: true }) }, { info: async () => {}, warning: async () => {}, error: async () => {} }); service.refreshProfileState = async () => ({ statusReachable: true, statusRepository: 'jens/app', statusEnvironment: 'production', liveSha: 'a'.repeat(40), previousSha }); await assert.rejects( service.rollback({ repository: { fullName: 'jens/app', localPath: '/repo' }, profileId: profile.id, targetSha: 'c'.repeat(40) }), /no longer the previous server version/i ); assert.equal(dispatched, false); });