github.com/hernad/nomad@v1.6.112/ui/tests/acceptance/allocation-detail-test.js (about)

     1  /**
     2   * Copyright (c) HashiCorp, Inc.
     3   * SPDX-License-Identifier: MPL-2.0
     4   */
     5  
     6  /* eslint-disable qunit/require-expect */
     7  /* Mirage fixtures are random so we can't expect a set number of assertions */
     8  import AdapterError from '@ember-data/adapter/error';
     9  import { run } from '@ember/runloop';
    10  import { currentURL, click, triggerEvent, waitFor } from '@ember/test-helpers';
    11  import { assign } from '@ember/polyfills';
    12  import { module, test } from 'qunit';
    13  import { setupApplicationTest } from 'ember-qunit';
    14  import { setupMirage } from 'ember-cli-mirage/test-support';
    15  import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
    16  import Allocation from 'nomad-ui/tests/pages/allocations/detail';
    17  import moment from 'moment';
    18  import formatHost from 'nomad-ui/utils/format-host';
    19  import faker from 'nomad-ui/mirage/faker';
    20  
    21  let job;
    22  let node;
    23  let allocation;
    24  
    25  module('Acceptance | allocation detail', function (hooks) {
    26    setupApplicationTest(hooks);
    27    setupMirage(hooks);
    28  
    29    hooks.beforeEach(async function () {
    30      server.create('agent');
    31  
    32      server.create('node-pool');
    33      node = server.create('node');
    34      job = server.create('job', {
    35        groupsCount: 1,
    36        withGroupServices: true,
    37        createAllocations: false,
    38      });
    39      allocation = server.create('allocation', 'withTaskWithPorts', {
    40        clientStatus: 'running',
    41      });
    42  
    43      // Make sure the node has an unhealthy driver
    44      node.update({
    45        driver: assign(node.drivers, {
    46          docker: {
    47            detected: true,
    48            healthy: false,
    49          },
    50        }),
    51      });
    52  
    53      // Make sure a task for the allocation depends on the unhealthy driver
    54      server.schema.tasks.first().update({
    55        driver: 'docker',
    56      });
    57  
    58      await Allocation.visit({ id: allocation.id });
    59    });
    60  
    61    test('it passes an accessibility audit', async function (assert) {
    62      await a11yAudit(assert);
    63    });
    64  
    65    test('/allocation/:id should name the allocation and link to the corresponding job and node', async function (assert) {
    66      assert.ok(
    67        Allocation.title.includes(allocation.name),
    68        'Allocation name is in the heading'
    69      );
    70      assert.equal(
    71        Allocation.details.job,
    72        job.name,
    73        'Job name is in the subheading'
    74      );
    75      assert.equal(
    76        Allocation.details.client,
    77        node.id.split('-')[0],
    78        'Node short id is in the subheading'
    79      );
    80      assert.ok(Allocation.execButton.isPresent);
    81  
    82      assert.ok(document.title.includes(`Allocation ${allocation.name} `));
    83  
    84      await Allocation.details.visitJob();
    85      assert.equal(
    86        currentURL(),
    87        `/jobs/${job.id}@default`,
    88        'Job link navigates to the job'
    89      );
    90  
    91      await Allocation.visit({ id: allocation.id });
    92  
    93      await Allocation.details.visitClient();
    94      assert.equal(
    95        currentURL(),
    96        `/clients/${node.id}`,
    97        'Client link navigates to the client'
    98      );
    99    });
   100  
   101    test('/allocation/:id should include resource utilization graphs', async function (assert) {
   102      assert.equal(
   103        Allocation.resourceCharts.length,
   104        2,
   105        'Two resource utilization graphs'
   106      );
   107      assert.equal(
   108        Allocation.resourceCharts.objectAt(0).name,
   109        'CPU',
   110        'First chart is CPU'
   111      );
   112      assert.equal(
   113        Allocation.resourceCharts.objectAt(1).name,
   114        'Memory',
   115        'Second chart is Memory'
   116      );
   117    });
   118  
   119    test('/allocation/:id should present task lifecycles', async function (assert) {
   120      const job = server.create('job', {
   121        groupsCount: 1,
   122        groupTaskCount: 6,
   123        withGroupServices: true,
   124        createAllocations: false,
   125      });
   126  
   127      const allocation = server.create('allocation', 'withTaskWithPorts', {
   128        clientStatus: 'running',
   129        jobId: job.id,
   130      });
   131  
   132      await Allocation.visit({ id: allocation.id });
   133  
   134      assert.ok(Allocation.lifecycleChart.isPresent);
   135      assert.equal(Allocation.lifecycleChart.title, 'Task Lifecycle Status');
   136      assert.equal(Allocation.lifecycleChart.phases.length, 4);
   137      assert.equal(Allocation.lifecycleChart.tasks.length, 6);
   138  
   139      await Allocation.lifecycleChart.tasks[0].visit();
   140  
   141      const prestartEphemeralTask = server.db.taskStates
   142        .where({ allocationId: allocation.id })
   143        .sortBy('name')
   144        .find((taskState) => {
   145          const task = server.db.tasks.findBy({ name: taskState.name });
   146          return (
   147            task.Lifecycle &&
   148            task.Lifecycle.Hook === 'prestart' &&
   149            !task.Lifecycle.Sidecar
   150          );
   151        });
   152  
   153      assert.equal(
   154        currentURL(),
   155        `/allocations/${allocation.id}/${prestartEphemeralTask.name}`
   156      );
   157    });
   158  
   159    test('/allocation/:id should list all tasks for the allocation', async function (assert) {
   160      assert.equal(
   161        Allocation.tasks.length,
   162        server.db.taskStates.where({ allocationId: allocation.id }).length,
   163        'Table lists all tasks'
   164      );
   165      assert.notOk(Allocation.isEmpty, 'Task table empty state is not shown');
   166    });
   167  
   168    test('each task row should list high-level information for the task', async function (assert) {
   169      const job = server.create('job', {
   170        groupsCount: 1,
   171        groupTaskCount: 3,
   172        withGroupServices: true,
   173        createAllocations: false,
   174      });
   175  
   176      const allocation = server.create('allocation', 'withTaskWithPorts', {
   177        clientStatus: 'running',
   178        jobId: job.id,
   179      });
   180  
   181      const taskGroup = server.schema.taskGroups.where({
   182        jobId: allocation.jobId,
   183        name: allocation.taskGroup,
   184      }).models[0];
   185  
   186      // Set the expected task states.
   187      const states = ['running', 'pending', 'dead'];
   188      server.db.taskStates
   189        .where({ allocationId: allocation.id })
   190        .sortBy('name')
   191        .forEach((task, i) => {
   192          server.db.taskStates.update(task.id, { state: states[i] });
   193        });
   194  
   195      await Allocation.visit({ id: allocation.id });
   196  
   197      Allocation.tasks.forEach((taskRow, i) => {
   198        const task = server.db.taskStates
   199          .where({ allocationId: allocation.id })
   200          .sortBy('name')[i];
   201        const events = server.db.taskEvents.where({ taskStateId: task.id });
   202        const event = events[events.length - 1];
   203  
   204        const jobTask = taskGroup.tasks.models.find((m) => m.name === task.name);
   205        const volumes = jobTask.volumeMounts.map((volume) => ({
   206          name: volume.Volume,
   207          source: taskGroup.volumes[volume.Volume].Source,
   208        }));
   209  
   210        assert.equal(taskRow.name, task.name, 'Name');
   211        assert.equal(taskRow.state, task.state, 'State');
   212        assert.equal(taskRow.message, event.message, 'Event Message');
   213        assert.equal(
   214          taskRow.time,
   215          moment(event.time / 1000000).format("MMM DD, 'YY HH:mm:ss ZZ"),
   216          'Event Time'
   217        );
   218  
   219        const expectStats = task.state === 'running';
   220        assert.equal(taskRow.hasCpuMetrics, expectStats, 'CPU metrics');
   221        assert.equal(taskRow.hasMemoryMetrics, expectStats, 'Memory metrics');
   222  
   223        const volumesText = taskRow.volumes;
   224        volumes.forEach((volume) => {
   225          assert.ok(
   226            volumesText.includes(volume.name),
   227            `Found label ${volume.name}`
   228          );
   229          assert.ok(
   230            volumesText.includes(volume.source),
   231            `Found value ${volume.source}`
   232          );
   233        });
   234      });
   235    });
   236  
   237    test('each task row should link to the task detail page', async function (assert) {
   238      const task = server.db.taskStates
   239        .where({ allocationId: allocation.id })
   240        .sortBy('name')[0];
   241  
   242      await Allocation.tasks.objectAt(0).clickLink();
   243  
   244      // Make sure the allocation is pending in order to ensure there are no tasks
   245      assert.equal(
   246        currentURL(),
   247        `/allocations/${allocation.id}/${task.name}`,
   248        'Task name in task row links to task detail'
   249      );
   250  
   251      await Allocation.visit({ id: allocation.id });
   252      await Allocation.tasks.objectAt(0).clickRow();
   253  
   254      assert.equal(
   255        currentURL(),
   256        `/allocations/${allocation.id}/${task.name}`,
   257        'Task row links to task detail'
   258      );
   259    });
   260  
   261    test('tasks with an unhealthy driver have a warning icon', async function (assert) {
   262      assert.ok(
   263        Allocation.firstUnhealthyTask().hasUnhealthyDriver,
   264        'Warning is shown'
   265      );
   266    });
   267  
   268    test('proxy task has a proxy tag', async function (assert) {
   269      // Must create a new job as existing one has loaded and it contains the tasks
   270      job = server.create('job', {
   271        groupsCount: 1,
   272        withGroupServices: true,
   273        createAllocations: false,
   274      });
   275  
   276      allocation = server.create('allocation', 'withTaskWithPorts', {
   277        clientStatus: 'running',
   278        jobId: job.id,
   279      });
   280  
   281      const taskState = allocation.taskStates.models.sortBy('name')[0];
   282      const task = server.schema.tasks.findBy({ name: taskState.name });
   283      task.update('kind', 'connect-proxy:task');
   284      task.save();
   285  
   286      await Allocation.visit({ id: allocation.id });
   287  
   288      assert.ok(Allocation.tasks[0].hasProxyTag);
   289    });
   290  
   291    test('when there are no tasks, an empty state is shown', async function (assert) {
   292      // Make sure the allocation is pending in order to ensure there are no tasks
   293      allocation = server.create('allocation', 'withTaskWithPorts', {
   294        clientStatus: 'pending',
   295      });
   296      await Allocation.visit({ id: allocation.id });
   297  
   298      assert.ok(Allocation.isEmpty, 'Task table empty state is shown');
   299    });
   300  
   301    test('when the allocation has not been rescheduled, the reschedule events section is not rendered', async function (assert) {
   302      assert.notOk(
   303        Allocation.hasRescheduleEvents,
   304        'Reschedule Events section exists'
   305      );
   306    });
   307  
   308    test('ports are listed', async function (assert) {
   309      const allServerPorts = allocation.taskResources.models[0].resources.Ports;
   310  
   311      allServerPorts.sortBy('Label').forEach((serverPort, index) => {
   312        const renderedPort = Allocation.ports[index];
   313  
   314        assert.equal(renderedPort.name, serverPort.Label);
   315        assert.equal(renderedPort.to, serverPort.To);
   316        assert.equal(
   317          renderedPort.address,
   318          formatHost(serverPort.HostIP, serverPort.Value)
   319        );
   320      });
   321    });
   322  
   323    test('services are listed', async function (assert) {
   324      const taskGroup = server.schema.taskGroups.findBy({
   325        name: allocation.taskGroup,
   326      });
   327  
   328      assert.equal(Allocation.services.length, taskGroup.services.length);
   329  
   330      taskGroup.services.models.sortBy('name').forEach((serverService, index) => {
   331        const renderedService = Allocation.services[index];
   332  
   333        assert.equal(renderedService.name, serverService.name);
   334        assert.equal(renderedService.port, serverService.portLabel);
   335        assert.equal(renderedService.tags, (serverService.tags || []).join(' '));
   336      });
   337    });
   338  
   339    test('when the allocation is not found, an error message is shown, but the URL persists', async function (assert) {
   340      await Allocation.visit({ id: 'not-a-real-allocation' });
   341  
   342      assert.equal(
   343        server.pretender.handledRequests
   344          .filter((request) => !request.url.includes('policy'))
   345          .findBy('status', 404).url,
   346        '/v1/allocation/not-a-real-allocation',
   347        'A request to the nonexistent allocation is made'
   348      );
   349      assert.equal(
   350        currentURL(),
   351        '/allocations/not-a-real-allocation',
   352        'The URL persists'
   353      );
   354      assert.ok(Allocation.error.isShown, 'Error message is shown');
   355      assert.equal(
   356        Allocation.error.title,
   357        'Not Found',
   358        'Error message is for 404'
   359      );
   360    });
   361  
   362    test('allocation can be stopped', async function (assert) {
   363      await Allocation.stop.idle();
   364      await Allocation.stop.confirm();
   365  
   366      assert.equal(
   367        server.pretender.handledRequests
   368          .reject((request) => request.url.includes('fuzzy'))
   369          .findBy('method', 'POST').url,
   370        `/v1/allocation/${allocation.id}/stop`,
   371        'Stop request is made for the allocation'
   372      );
   373    });
   374  
   375    test('allocation can be restarted', async function (assert) {
   376      await Allocation.restartAll.idle();
   377      await Allocation.restart.idle();
   378      await Allocation.restart.confirm();
   379  
   380      assert.equal(
   381        server.pretender.handledRequests.findBy('method', 'PUT').url,
   382        `/v1/client/allocation/${allocation.id}/restart`,
   383        'Restart request is made for the allocation'
   384      );
   385  
   386      await Allocation.restart.idle();
   387      await Allocation.restartAll.idle();
   388      await Allocation.restartAll.confirm();
   389  
   390      assert.ok(
   391        server.pretender.handledRequests.filterBy(
   392          'requestBody',
   393          JSON.stringify({ AllTasks: true })
   394        ),
   395        'Restart all tasks request is made for the allocation'
   396      );
   397    });
   398  
   399    test('while an allocation is being restarted, the stop button is disabled', async function (assert) {
   400      server.pretender.post('/v1/allocation/:id/stop', () => [204, {}, ''], true);
   401  
   402      await Allocation.stop.idle();
   403  
   404      run.later(() => {
   405        assert.ok(Allocation.stop.isRunning, 'Stop is loading');
   406        assert.ok(Allocation.restart.isDisabled, 'Restart is disabled');
   407        assert.ok(Allocation.restartAll.isDisabled, 'Restart All is disabled');
   408        server.pretender.resolve(server.pretender.requestReferences[0].request);
   409      }, 500);
   410  
   411      await Allocation.stop.confirm();
   412    });
   413  
   414    test('if stopping or restarting fails, an error message is shown', async function (assert) {
   415      server.pretender.post('/v1/allocation/:id/stop', () => [403, {}, '']);
   416  
   417      await Allocation.stop.idle();
   418      await Allocation.stop.confirm();
   419  
   420      assert.ok(Allocation.inlineError.isShown, 'Inline error is shown');
   421      assert.ok(
   422        Allocation.inlineError.title.includes('Could Not Stop Allocation'),
   423        'Title is descriptive'
   424      );
   425      assert.ok(
   426        /ACL token.+?allocation lifecycle/.test(Allocation.inlineError.message),
   427        'Message mentions ACLs and the appropriate permission'
   428      );
   429  
   430      await Allocation.inlineError.dismiss();
   431  
   432      assert.notOk(
   433        Allocation.inlineError.isShown,
   434        'Inline error is no longer shown'
   435      );
   436    });
   437  
   438    test('when navigating to an allocation, if the allocation no longer exists it does a redirect to previous page', async function (assert) {
   439      await click('[data-test-breadcrumb="jobs.job.index"]');
   440      await click('[data-test-tab="allocations"] > a');
   441  
   442      const component = this.owner.lookup('component:allocation-row');
   443      const router = this.owner.lookup('service:router');
   444      const allocRoute = this.owner.lookup('route:allocations.allocation');
   445      const originalMethod = allocRoute.goBackToReferrer;
   446      allocRoute.goBackToReferrer = () => {
   447        assert.step('Transition dispatched.');
   448        router.transitionTo('jobs.job.allocations');
   449      };
   450  
   451      component.onClick = () =>
   452        router.transitionTo('allocations.allocation', 'aaa');
   453  
   454      server.get('/allocation/:id', function () {
   455        return new AdapterError([
   456          {
   457            detail: `alloc not found`,
   458            status: 404,
   459          },
   460        ]);
   461      });
   462  
   463      component.onClick();
   464  
   465      await waitFor('.flash-message.alert-critical');
   466  
   467      assert.verifySteps(['Transition dispatched.']);
   468      assert
   469        .dom('.flash-message.alert-critical')
   470        .exists('A toast error message pops up.');
   471  
   472      // Clean-up
   473      allocRoute.goBackToReferrer = originalMethod;
   474    });
   475  });
   476  
   477  module('Acceptance | allocation detail (rescheduled)', function (hooks) {
   478    setupApplicationTest(hooks);
   479    setupMirage(hooks);
   480  
   481    hooks.beforeEach(async function () {
   482      server.create('agent');
   483  
   484      server.create('node-pool');
   485      node = server.create('node');
   486      job = server.create('job', { createAllocations: false });
   487      allocation = server.create('allocation', 'rescheduled');
   488  
   489      await Allocation.visit({ id: allocation.id });
   490    });
   491  
   492    test('when the allocation has been rescheduled, the reschedule events section is rendered', async function (assert) {
   493      assert.ok(
   494        Allocation.hasRescheduleEvents,
   495        'Reschedule Events section exists'
   496      );
   497    });
   498  });
   499  
   500  module('Acceptance | allocation detail (not running)', function (hooks) {
   501    setupApplicationTest(hooks);
   502    setupMirage(hooks);
   503  
   504    hooks.beforeEach(async function () {
   505      server.create('agent');
   506  
   507      server.create('node-pool');
   508      node = server.create('node');
   509      job = server.create('job', { createAllocations: false });
   510      allocation = server.create('allocation', { clientStatus: 'pending' });
   511  
   512      await Allocation.visit({ id: allocation.id });
   513    });
   514  
   515    test('when the allocation is not running, the utilization graphs are replaced by an empty message', async function (assert) {
   516      assert.equal(Allocation.resourceCharts.length, 0, 'No resource charts');
   517      assert.equal(
   518        Allocation.resourceEmptyMessage,
   519        "Allocation isn't running",
   520        'Empty message is appropriate'
   521      );
   522    });
   523  
   524    test('the exec and stop/restart buttons are absent', async function (assert) {
   525      assert.notOk(Allocation.execButton.isPresent);
   526      assert.notOk(Allocation.stop.isPresent);
   527      assert.notOk(Allocation.restart.isPresent);
   528      assert.notOk(Allocation.restartAll.isPresent);
   529    });
   530  });
   531  
   532  module('Acceptance | allocation detail (preemptions)', function (hooks) {
   533    setupApplicationTest(hooks);
   534    setupMirage(hooks);
   535  
   536    hooks.beforeEach(async function () {
   537      server.create('agent');
   538      server.create('node-pool');
   539      node = server.create('node');
   540      job = server.create('job', { createAllocations: false });
   541    });
   542  
   543    test('shows a dedicated section to the allocation that preempted this allocation', async function (assert) {
   544      allocation = server.create('allocation', 'preempted');
   545      const preempter = server.schema.find(
   546        'allocation',
   547        allocation.preemptedByAllocation
   548      );
   549      const preempterJob = server.schema.find('job', preempter.jobId);
   550      const preempterClient = server.schema.find('node', preempter.nodeId);
   551  
   552      await Allocation.visit({ id: allocation.id });
   553      assert.ok(Allocation.wasPreempted, 'Preempted allocation section is shown');
   554      assert.equal(
   555        Allocation.preempter.status,
   556        preempter.clientStatus,
   557        'Preempter status matches'
   558      );
   559      assert.equal(
   560        Allocation.preempter.name,
   561        preempter.name,
   562        'Preempter name matches'
   563      );
   564      assert.equal(
   565        Allocation.preempter.priority,
   566        preempterJob.priority,
   567        'Preempter priority matches'
   568      );
   569  
   570      await Allocation.preempter.visit();
   571      assert.equal(
   572        currentURL(),
   573        `/allocations/${preempter.id}`,
   574        'Clicking the preempter id navigates to the preempter allocation detail page'
   575      );
   576  
   577      await Allocation.visit({ id: allocation.id });
   578  
   579      await Allocation.preempter.visitJob();
   580      assert.equal(
   581        currentURL(),
   582        `/jobs/${preempterJob.id}@default`,
   583        'Clicking the preempter job link navigates to the preempter job page'
   584      );
   585  
   586      await Allocation.visit({ id: allocation.id });
   587      await Allocation.preempter.visitClient();
   588      assert.equal(
   589        currentURL(),
   590        `/clients/${preempterClient.id}`,
   591        'Clicking the preempter client link navigates to the preempter client page'
   592      );
   593    });
   594  
   595    test('shows a dedicated section to the allocations this allocation preempted', async function (assert) {
   596      allocation = server.create('allocation', 'preempter');
   597      await Allocation.visit({ id: allocation.id });
   598      assert.ok(
   599        Allocation.preempted,
   600        'The allocations this allocation preempted are shown'
   601      );
   602    });
   603  
   604    test('each preempted allocation in the table lists basic allocation information', async function (assert) {
   605      allocation = server.create('allocation', 'preempter');
   606      await Allocation.visit({ id: allocation.id });
   607  
   608      const preemption = allocation.preemptedAllocations
   609        .map((id) => server.schema.find('allocation', id))
   610        .sortBy('modifyIndex')
   611        .reverse()[0];
   612      const preemptionRow = Allocation.preemptions.objectAt(0);
   613  
   614      assert.equal(
   615        Allocation.preemptions.length,
   616        allocation.preemptedAllocations.length,
   617        'The preemptions table has a row for each preempted allocation'
   618      );
   619  
   620      assert.equal(
   621        preemptionRow.shortId,
   622        preemption.id.split('-')[0],
   623        'Preemption short id'
   624      );
   625      assert.equal(
   626        preemptionRow.createTime,
   627        moment(preemption.createTime / 1000000).format('MMM DD HH:mm:ss ZZ'),
   628        'Preemption create time'
   629      );
   630      assert.equal(
   631        preemptionRow.modifyTime,
   632        moment(preemption.modifyTime / 1000000).fromNow(),
   633        'Preemption modify time'
   634      );
   635      assert.equal(
   636        preemptionRow.status,
   637        preemption.clientStatus,
   638        'Client status'
   639      );
   640      assert.equal(
   641        preemptionRow.jobVersion,
   642        preemption.jobVersion,
   643        'Job Version'
   644      );
   645      assert.equal(
   646        preemptionRow.client,
   647        server.db.nodes.find(preemption.nodeId).id.split('-')[0],
   648        'Node ID'
   649      );
   650  
   651      await preemptionRow.visitClient();
   652      assert.equal(
   653        currentURL(),
   654        `/clients/${preemption.nodeId}`,
   655        'Node links to node page'
   656      );
   657    });
   658  
   659    test('when an allocation both preempted allocations and was preempted itself, both preemptions sections are shown', async function (assert) {
   660      allocation = server.create('allocation', 'preempter', 'preempted');
   661      await Allocation.visit({ id: allocation.id });
   662      assert.ok(
   663        Allocation.preempted,
   664        'The allocations this allocation preempted are shown'
   665      );
   666      assert.ok(Allocation.wasPreempted, 'Preempted allocation section is shown');
   667    });
   668  });
   669  
   670  module('Acceptance | allocation detail (services)', function (hooks) {
   671    setupApplicationTest(hooks);
   672    setupMirage(hooks);
   673  
   674    hooks.beforeEach(async function () {
   675      server.create('feature', { name: 'Dynamic Application Sizing' });
   676      server.createList('agent', 3, 'withConsulLink', 'withVaultLink');
   677      server.createList('node-pool', 3);
   678      server.createList('node', 5);
   679      server.createList('job', 1, { createRecommendations: true });
   680      const job = server.create('job', {
   681        withGroupServices: true,
   682        withTaskServices: true,
   683        name: 'Service-haver',
   684        id: 'service-haver',
   685        namespaceId: 'default',
   686      });
   687  
   688      const runningAlloc = server.create('allocation', {
   689        jobId: job.id,
   690        forceRunningClientStatus: true,
   691        clientStatus: 'running',
   692      });
   693      const otherAlloc = server.db.allocations.reject((j) => j.jobId !== job.id);
   694  
   695      server.db.serviceFragments.update({
   696        healthChecks: [
   697          {
   698            Status: 'success',
   699            Check: 'check1',
   700            Timestamp: 99,
   701            Alloc: runningAlloc.id,
   702          },
   703          {
   704            Status: 'failure',
   705            Check: 'check2',
   706            Output: 'One',
   707            propThatDoesntMatter:
   708              'this object will be ignored, since it shared a Check name with a later one.',
   709            Timestamp: 98,
   710            Alloc: runningAlloc.id,
   711          },
   712          {
   713            Status: 'success',
   714            Check: 'check2',
   715            Output: 'Two',
   716            Timestamp: 99,
   717            Alloc: runningAlloc.id,
   718          },
   719          {
   720            Status: 'failure',
   721            Check: 'check3',
   722            Output: 'Oh no!',
   723            Timestamp: 99,
   724            Alloc: runningAlloc.id,
   725          },
   726          {
   727            Status: 'success',
   728            Check: 'check3',
   729            Output: 'Wont be seen',
   730            propThatDoesntMatter:
   731              'this object will be ignored, in spite of its later timestamp, since it exists on a different alloc',
   732            Timestamp: 100,
   733            Alloc: otherAlloc.id,
   734          },
   735        ],
   736      });
   737    });
   738  
   739    test('Allocation has a list of services with active checks', async function (assert) {
   740      faker.seed(1);
   741      const runningAlloc = server.db.allocations.findBy({
   742        jobId: 'service-haver',
   743        forceRunningClientStatus: true,
   744        clientStatus: 'running',
   745      });
   746      await Allocation.visit({ id: runningAlloc.id });
   747      assert.dom('[data-test-service]').exists();
   748      assert.dom('.service-sidebar').exists();
   749      assert.dom('.service-sidebar').doesNotHaveClass('open');
   750      assert
   751        .dom('[data-test-service-status-bar]')
   752        .exists('At least one allocation has service health');
   753  
   754      await click('[data-test-service-status-bar]');
   755      assert.dom('.service-sidebar').hasClass('open');
   756      assert
   757        .dom('table.health-checks tr[data-service-health="success"]')
   758        .exists({ count: 2 }, 'Two successful health checks');
   759      assert
   760        .dom('table.health-checks tr[data-service-health="failure"]')
   761        .exists({ count: 1 }, 'One failing health check');
   762      assert
   763        .dom(
   764          'table.health-checks tr[data-service-health="failure"] td.service-output'
   765        )
   766        .containsText('Oh no!');
   767  
   768      await triggerEvent('.page-layout', 'keydown', { key: 'Escape' });
   769      assert.dom('.service-sidebar').doesNotHaveClass('open');
   770    });
   771  });