Code review comment: Otherwise, an application with two upgrade targets (due to previous failed deployments, or, later, due to an aborted change) would start tests for the other target immediately after completing them for the first, disregarding the delay.

Code under review:
private List<Job> computeReadyJobs(ApplicationId id) {
        List<Job> jobs = new ArrayList<>();
        applications().get(id).ifPresent(application -> {
            List<Step> steps = application.deploymentSpec().steps().isEmpty()
                    ? singletonList(new DeploymentSpec.DeclaredZone(test))
                    : application.deploymentSpec().steps();
            List<Step> productionSteps = steps.stream().filter(step -> step.deploysTo(prod) || step.zones().isEmpty()).collect(toList());

            Optional<Instant> completedAt = application.deploymentJobs().statusOf(stagingTest)
                                                       .flatMap(JobStatus::lastSuccess).map(JobRun::at);
            String reason = "New change available";
            List<Job> testJobs = null;

            for (Step step : productionSteps) {
                Set<JobType> stepJobs = step.zones().stream().map(order::toJob).collect(toSet());
                Map<Optional<Instant>, List<JobType>> jobsByCompletion = stepJobs.stream().collect(groupingBy(job -> completedAt(application.change(), application, job)));
                if (jobsByCompletion.containsKey(empty())) { 
                    for (JobType job : jobsByCompletion.get(empty())) {
                        State target = targetFor(application, application.change(), deploymentFor(application, job));
                        if (isVerified(application, target, job)) {
                            if (completedAt.isPresent())
                                jobs.add(deploymentJob(application, target, application.change(), job, reason, completedAt.get(), stepJobs));
                        }
                        else if (testJobs == null) {
                            if ( ! alreadyTriggered(application, target))
                                testJobs = testJobsFor(application, target, "Testing deployment for " + job.jobName(), completedAt.orElse(clock.instant()));
                        }
                    }
                }
                else { 
                    if (stepJobs.isEmpty()) { 
                        Duration delay = ((DeploymentSpec.Delay) step).duration();
                        completedAt = completedAt.map(at -> at.plus(delay)).filter(at -> ! at.isAfter(clock.instant()));
                        reason += " after a delay of " + delay;
                    }
                    else {
                        completedAt = jobsByCompletion.keySet().stream().map(Optional::get).max(naturalOrder());
                        reason = "Available change in " + stepJobs.stream().map(JobType::jobName).collect(joining(", "));
                    }
                }
            }

            if (testJobs == null)
                testJobs = testJobsFor(application, targetFor(application, application.change(), empty()), "Testing last changes outside prod", clock.instant());
            jobs.addAll(testJobs);

            
            if (steps.stream().flatMap(step -> step.zones().stream()).map(order::toJob)
                     .allMatch(job -> completedAt(application.change(), application, job).isPresent()))
                applications().lockIfPresent(id, lockedApplication -> applications().store(lockedApplication.withChange(Change.empty())));
        });
        return jobs;
    }

Line commented on: if ( ! alreadyTriggered(application, target))

Revised code:
private List<Job> computeReadyJobs(ApplicationId id) {
        List<Job> jobs = new ArrayList<>();
        applications().get(id).ifPresent(application -> {
            List<Step> steps = application.deploymentSpec().steps().isEmpty()
                    ? singletonList(new DeploymentSpec.DeclaredZone(test))
                    : application.deploymentSpec().steps();
            List<Step> productionSteps = steps.stream().filter(step -> step.deploysTo(prod) || step.zones().isEmpty()).collect(toList());

            Optional<Instant> completedAt = application.deploymentJobs().statusOf(stagingTest)
                                                       .flatMap(JobStatus::lastSuccess).map(JobRun::at);
            String reason = "New change available";
            List<Job> testJobs = null;

            for (Step step : productionSteps) {
                Set<JobType> stepJobs = step.zones().stream().map(order::toJob).collect(toSet());
                Map<Optional<Instant>, List<JobType>> jobsByCompletion = stepJobs.stream().collect(groupingBy(job -> completedAt(application.change(), application, job)));
                if (jobsByCompletion.containsKey(empty())) { 
                    for (JobType job : jobsByCompletion.get(empty())) {
                        State target = targetFor(application, application.change(), deploymentFor(application, job));
                        if (isVerified(application, target, job)) {
                            if (completedAt.isPresent())
                                jobs.add(deploymentJob(application, target, application.change(), job, reason, completedAt.get(), stepJobs));
                        }
                        else if (testJobs == null) {
                            if ( ! alreadyTriggered(application, target))
                                testJobs = testJobsFor(application, target, "Testing deployment for " + job.jobName(), completedAt.orElse(clock.instant()));
                            else
                                testJobs = emptyList();
                        }
                    }
                }
                else { 
                    if (stepJobs.isEmpty()) { 
                        Duration delay = ((DeploymentSpec.Delay) step).duration();
                        completedAt = completedAt.map(at -> at.plus(delay)).filter(at -> ! at.isAfter(clock.instant()));
                        reason += " after a delay of " + delay;
                    }
                    else {
                        completedAt = jobsByCompletion.keySet().stream().map(Optional::get).max(naturalOrder());
                        reason = "Available change in " + stepJobs.stream().map(JobType::jobName).collect(joining(", "));
                    }
                }
            }

            if (testJobs == null)
                testJobs = testJobsFor(application, targetFor(application, application.change(), empty()), "Testing last changes outside prod", clock.instant());
            jobs.addAll(testJobs);

            
            if (steps.stream().flatMap(step -> step.zones().stream()).map(order::toJob)
                     .allMatch(job -> completedAt(application.change(), application, job).isPresent()))
                applications().lockIfPresent(id, lockedApplication -> applications().store(lockedApplication.withChange(Change.empty())));
        });
        return jobs;
    }