solverforge-cli 1.1.2

CLI for scaffolding and managing SolverForge constraint solver projects
const { test, expect } = require('@playwright/test');
const { readManifest } = require('./harness');

test.describe('Standard Solver Pipeline', () => {
  test('runs retained job pause/resume/cancel/delete through the generated UI', async ({ page }) => {
    const { scenarios } = readManifest();
    const scenario = scenarios.standard;

    await test.step('Open generated standard app', async () => {
      await page.goto(scenario.baseUrl);
      await page.waitForSelector('#sf-app');
    });

    await test.step('Verify seeded standard demo data exists', async () => {
      const counts = await page.evaluate(async () => {
        const demo = await fetch('/demo-data/STANDARD').then((response) => response.json());
        return {
          resources: (demo.resources || []).length,
          tasks: (demo.tasks || []).length,
        };
      });
      expect(counts.resources).toBeGreaterThanOrEqual(8);
      expect(counts.tasks).toBeGreaterThanOrEqual(48);
    });

    await test.step('Solve, pause, inspect retained snapshot, cancel, and delete', async () => {
      const solveButton = page.getByRole('button', { name: 'Solve' });
      const pauseButton = page.getByRole('button', { name: 'Pause' });
      const resumeButton = page.getByRole('button', { name: 'Resume' });
      const cancelButton = page.getByRole('button', { name: 'Cancel' });

      await expect(solveButton).toBeVisible();
      await expect(pauseButton).toBeHidden();
      await expect(resumeButton).toBeHidden();
      await expect(cancelButton).toBeHidden();

      await solveButton.click();
      await expect(pauseButton).toBeVisible();
      await expect(cancelButton).toBeVisible();
      await expect(solveButton).toBeHidden();

      await page.waitForFunction(() => {
        const score = document.getElementById('sfScoreDisplay');
        const app = document.getElementById('sf-app');
        return !!score && score.textContent && score.textContent.trim() !== ''
          && !!app && !!app.dataset.jobId;
      });

      const liveJobId = await page.locator('#sf-app').evaluate((el) => el.dataset.jobId);
      expect(liveJobId).toBeTruthy();

      await pauseButton.click();
      await page.waitForFunction(() => {
        const app = document.getElementById('sf-app');
        return !!app && app.dataset.lifecycleState === 'PAUSED' && !!app.dataset.snapshotRevision;
      });

      await expect(resumeButton).toBeVisible();
      await expect(pauseButton).toBeHidden();

      const paused = await page.evaluate(async () => {
        const app = document.getElementById('sf-app');
        const jobId = app.dataset.jobId;
        const snapshotRevision = app.dataset.snapshotRevision;
        const summary = await fetch(`/jobs/${jobId}`).then((response) => response.json());
        const snapshot = await fetch(`/jobs/${jobId}/snapshot?snapshot_revision=${snapshotRevision}`).then((response) => response.json());
        const analysis = await fetch(`/jobs/${jobId}/analysis?snapshot_revision=${snapshotRevision}`).then((response) => response.json());
        return { jobId, snapshotRevision: Number(snapshotRevision), summary, snapshot, analysis };
      });

      expect(paused.summary.lifecycleState).toBe('PAUSED');
      expect(paused.summary.checkpointAvailable).toBeTruthy();
      expect(paused.snapshot.snapshotRevision).toBe(paused.snapshotRevision);
      expect(Array.isArray(paused.snapshot.solution.resources)).toBeTruthy();
      expect(Array.isArray(paused.snapshot.solution.tasks)).toBeTruthy();
      expect(Array.isArray(paused.analysis.analysis.constraints)).toBeTruthy();

      await cancelButton.click();
      await page.waitForFunction(() => {
        const app = document.getElementById('sf-app');
        return !!app && app.dataset.lifecycleState === 'CANCELLED';
      });

      await expect(solveButton).toBeVisible({ timeout: 10000 });
      await expect(pauseButton).toBeHidden();
      await expect(resumeButton).toBeHidden();

      const cancelled = await page.evaluate(async (jobId) => {
        const summary = await fetch(`/jobs/${jobId}`).then((response) => response.json());
        const deleteResponse = await fetch(`/jobs/${jobId}`, { method: 'DELETE' });
        const afterDelete = await fetch(`/jobs/${jobId}`);
        return {
          summary,
          deleteStatus: deleteResponse.status,
          afterDeleteStatus: afterDelete.status,
        };
      }, liveJobId);

      expect(cancelled.summary.lifecycleState).toBe('CANCELLED');
      expect(cancelled.summary.terminalReason).toBe('cancelled');
      expect(cancelled.deleteStatus).toBe(204);
      expect(cancelled.afterDeleteStatus).toBe(404);
    });

    await test.step('Solve again and resume the same retained job', async () => {
      const solveButton = page.getByRole('button', { name: 'Solve' });
      const pauseButton = page.getByRole('button', { name: 'Pause' });
      const resumeButton = page.getByRole('button', { name: 'Resume' });

      await expect(solveButton).toBeVisible();
      await solveButton.click();
      await expect(pauseButton).toBeVisible();

      await page.waitForFunction(() => {
        const score = document.getElementById('sfScoreDisplay');
        const app = document.getElementById('sf-app');
        return !!score && score.textContent && score.textContent.trim() !== ''
          && !!app && !!app.dataset.jobId;
      });

      const resumedJobId = await page.locator('#sf-app').evaluate((el) => el.dataset.jobId);
      expect(resumedJobId).toBeTruthy();

      await pauseButton.click();
      await page.waitForFunction((jobId) => {
        const app = document.getElementById('sf-app');
        return !!app
          && app.dataset.jobId === jobId
          && app.dataset.lifecycleState === 'PAUSED'
          && !!app.dataset.snapshotRevision;
      }, resumedJobId);

      await expect(resumeButton).toBeVisible();
      await resumeButton.click();

      await page.waitForFunction((jobId) => {
        const app = document.getElementById('sf-app');
        if (!app || app.dataset.jobId !== jobId) return false;
        return !!app.dataset.lifecycleState && app.dataset.lifecycleState !== 'PAUSED';
      }, resumedJobId);

      const resumed = await page.evaluate(async (jobId) => {
        const summary = await fetch(`/jobs/${jobId}`).then((response) => response.json());
        return {
          jobId,
          lifecycleState: summary.lifecycleState,
          summaryJobId: String(summary.id || summary.jobId || ''),
        };
      }, resumedJobId);

      expect(resumed.summaryJobId).toBe(resumed.jobId);
      expect(resumed.lifecycleState).not.toBe('PAUSED');
    });
  });
});