Skip to content

chore: bump cli-extension-cos and drop --experimental from cos acceptance tests - #7147

Open
joaoabreu-snyk wants to merge 1 commit into
mainfrom
chore/bump-cli-extension-cos-drop-experimental
Open

chore: bump cli-extension-cos and drop --experimental from cos acceptance tests#7147
joaoabreu-snyk wants to merge 1 commit into
mainfrom
chore/bump-cli-extension-cos-drop-experimental

Conversation

@joaoabreu-snyk

Copy link
Copy Markdown

What does this change do?

Bumps cli-extension-cos to v0.0.0-20260818092334-df22b398cfc0, which:

  • Drops the --experimental gate from every snyk cos command (passing the flag now fails as an unknown flag).
  • Makes the root cos command group visible in the top-level snyk --help.

Updates the cos acceptance suite (test/jest/acceptance/snyk-cos/cos.spec.ts) to match the new contract:

  • Removes the experimental gate test (the gate no longer exists).
  • Strips --experimental from every invocation.

Verification

  • Private CLI builds with the new extension (make build BUILD_MODE=private).
  • The built binary confirms --experimental is gone from cos commands and the cos group is present.
  • All 36 cos acceptance tests pass against the built binary.
  • go mod verify passes and go mod tidy is a no-op.

…ance tests

Bumps cli-extension-cos to v0.0.0-20260818092334-df22b398cfc0, which drops
the --experimental gate from every cos command and makes the root cos group
visible in top-level help.

Updates the cos acceptance suite to match: removes the experimental-gate test
and strips --experimental from every invocation, since passing it now fails as
an unknown flag.
@joaoabreu-snyk
joaoabreu-snyk requested a review from a team as a code owner August 18, 2026 10:02
@snyk-io

snyk-io Bot commented Aug 18, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues
Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@github-actions

Copy link
Copy Markdown
Contributor
Warnings
⚠️

"chore: bump cli-extension-cos and drop --experimental from cos acceptance tests" is too long. Keep the first line of your commit message under 72 characters.

Generated by 🚫 dangerJS against ba04ce8

@snyk-pr-review-bot

Copy link
Copy Markdown

PR Reviewer Guide 🔍

🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Interface Change 🟡 [minor]

The PR removes the --experimental flag requirement from all snyk cos commands and states that passing the flag now fails as an 'unknown flag'. While consistent with GA promotion, any existing automation scripts (CI/CD, cron jobs) that currently include this flag will break until updated. Consider making the flag a no-op instead of a failure to ease the transition.

    server.setEndpointResponse(tenantsPath, {
      data: [{ type: 'tenant', id: tenantId, attributes: { name: 'Acme' } }],
    });
    server.setEndpointResponse(scansPath, {
      data: [scanResource()],
      links: { self: scansPath },
    });

    const { code, stdout } = await runSnykCLI(`cos scan list`, { env });
    expect(code).toEqual(0);
    expect(stdout).toContain(scanId);

    expect(tenantRequests()).toEqual([{ method: 'GET', path: tenantsPath }]);
    // The discovered tenant scopes the request: its id is part of the path.
    expect(cosRequests()).toEqual([{ method: 'GET', path: scansPath }]);
  });
});

describe('cos scan list', () => {
  test('lists scans in a table', async () => {
    server.setEndpointResponse(scansPath, {
      data: [scanResource()],
      links: { self: scansPath },
    });

    const { code, stdout } = await runSnykCLI(
      `cos scan list --tenant-id=${tenantId}`,
      { env },
    );
    expect(code).toEqual(0);
    expect(stdout).toContain('ID');
    expect(stdout).toContain('STATUS');
    expect(stdout).toContain(scanId);
    expect(stdout).toContain('running');

    expect(cosRequests()).toEqual([{ method: 'GET', path: scansPath }]);
  });

  test('renders JSON with -o json', async () => {
    server.setEndpointResponse(scansPath, {
      data: [scanResource()],
      links: { self: scansPath },
    });

    const { code, stdout } = await runSnykCLI(
      `cos scan list --tenant-id=${tenantId} -o json`,
      { env },
    );
    expect(code).toEqual(0);

    const parsed = JSON.parse(stdout);
    expect(parsed.scans).toHaveLength(1);
    expect(parsed.scans[0]).toMatchObject({
      id: scanId,
      status: 'running',
    });
  });

  test('handles an empty result', async () => {
    server.setEndpointResponse(scansPath, {
      data: [],
      links: { self: scansPath },
    });

    const { code, stdout } = await runSnykCLI(
      `cos scan list --tenant-id=${tenantId}`,
      { env },
    );
    expect(code).toEqual(0);
    expect(stdout).toContain('No scans found.');
  });

  test('surfaces a server error', async () => {
    server.setEndpointResponse(scansPath, {
      errors: [{ status: '500', title: 'Internal Server Error' }],
    });
    server.setEndpointStatusCode(scansPath, 500);

    // --max-attempts=1 disables network retries so the 500 fails fast.
    const { code } = await runSnykCLI(
      `cos scan list --tenant-id=${tenantId} --max-attempts=1`,
      { env },
    );
    expect(code).toEqual(2);
  });
});

describe('cos scan start', () => {
  test('creates a scan for a target', async () => {
    server.setEndpointResponse(scansPath, {
      data: scanResource({ status: 'queued' }),
      links: { self: scanPath },
    });

    const { code, stdout } = await runSnykCLI(
      `cos scan start --target-id=${targetId} --tenant-id=${tenantId}`,
      { env },
    );
    expect(code).toEqual(0);
    expect(stdout).toContain('Scan started');
    expect(stdout).toContain(scanId);

    expect(cosRequests()).toEqual([{ method: 'POST', path: scansPath }]);
  });

  test('fails without --target-id', async () => {
    const { code, stdout } = await runSnykCLI(
      `cos scan start --tenant-id=${tenantId}`,
      { env },
    );
    expect(code).toEqual(2);
    expect(stdout).toContain('--target-id');
    // Validation happens before any API call.
    expect(cosRequests()).toEqual([]);
  });
});

describe('cos scan status', () => {
  test('reports the status of a scan', async () => {
    server.setEndpointResponse(scanPath, {
      data: scanResource({ status: 'completed' }),
      links: { self: scanPath },
    });

    const { code, stdout } = await runSnykCLI(
      `cos scan status --tenant-id=${tenantId} --scan-id=${scanId}`,
      { env },
    );
    expect(code).toEqual(0);
    expect(stdout).toContain('Scan');
    expect(stdout).toContain(scanId);
    expect(stdout).toContain('completed');

    expect(cosRequests()).toEqual([{ method: 'GET', path: scanPath }]);
  });

  test('renders JSON with --json', async () => {
    server.setEndpointResponse(scanPath, {
      data: scanResource({ status: 'completed' }),
      links: { self: scanPath },
    });

    const { code, stdout } = await runSnykCLI(
      `cos scan status --tenant-id=${tenantId} --scan-id=${scanId} --json`,
      { env },
    );
    expect(code).toEqual(0);

    const parsed = JSON.parse(stdout);
    expect(parsed).toMatchObject({
      id: scanId,
      status: 'completed',
    });
  });

  test('fails without --scan-id', async () => {
    const { code, stdout } = await runSnykCLI(
      `cos scan status --tenant-id=${tenantId}`,
      { env },
    );
    expect(code).toEqual(2);
    expect(stdout).toContain('--scan-id');
    expect(cosRequests()).toEqual([]);
  });
});

describe('cos scan cancel', () => {
  test('cancels a scan', async () => {
    const cancelPath = `${scanPath}/cancel`;
    server.setEndpointResponse(cancelPath, {
      data: scanResource({ status: 'canceled' }),
      links: { self: scanPath },
    });

    const { code, stdout } = await runSnykCLI(
      `cos scan cancel --tenant-id=${tenantId} --scan-id=${scanId}`,
      { env },
    );
    expect(code).toEqual(0);
    expect(stdout).toContain('canceled');
    expect(stdout).toContain(scanId);

    expect(cosRequests()).toEqual([{ method: 'POST', path: cancelPath }]);
  });

  test('fails without --scan-id', async () => {
    const { code, stdout } = await runSnykCLI(
      `cos scan cancel --tenant-id=${tenantId}`,
      { env },
    );
    expect(code).toEqual(2);
    expect(stdout).toContain('--scan-id');
    expect(cosRequests()).toEqual([]);
  });
});

describe('cos scan report', () => {
  test('prints the report JSON', async () => {
    const reportPath = `${scanPath}/report`;
    server.setEndpointResponse(reportPath, {
      data: {
        type: 'scan_report',
        id: scanId,
        attributes: { summary: { issues: 2 }, findings: ['a', 'b'] },
      },
    });

    const { code, stdout } = await runSnykCLI(
      `cos scan report --tenant-id=${tenantId} --scan-id=${scanId}`,
      { env },
    );
    expect(code).toEqual(0);

    const parsed = JSON.parse(stdout);
    expect(parsed).toMatchObject({
      summary: { issues: 2 },
      findings: ['a', 'b'],
    });

    expect(cosRequests()).toEqual([{ method: 'GET', path: reportPath }]);
  });

  test('writes the report to --output-file', async () => {
    const reportPath = `${scanPath}/report`;
    server.setEndpointResponse(reportPath, {
      data: {
        type: 'scan_report',
        id: scanId,
        attributes: { summary: { issues: 0 } },
      },
    });

    const outFile = join(makeTmpDir(), 'report.json');

    const { code } = await runSnykCLI(
      `cos scan report --tenant-id=${tenantId} --scan-id=${scanId} --output-file=${outFile}`,
      { env },
    );
    expect(code).toEqual(0);

    const written = JSON.parse(readFileSync(outFile, 'utf-8'));
    expect(written).toMatchObject({ summary: { issues: 0 } });
  });

  test('downloads the PDF report with -o pdf', async () => {
    const pdfPath = `${scanPath}/report/pdf`;
    const pdfBody = '%PDF-1.4 pretend report';
    server.setEndpointHeaders(pdfPath, { 'Content-Type': 'application/pdf' });
    server.setEndpointResponse(pdfPath, pdfBody);

    const outFile = join(makeTmpDir(), 'report.pdf');

    const { code, stdout } = await runSnykCLI(
      `cos scan report --tenant-id=${tenantId} --scan-id=${scanId} -o pdf --output-file=${outFile}`,
      { env },
    );
    expect(code).toEqual(0);
    // A binary report is written to disk, with only a summary on stdout.
    expect(stdout).toContain(outFile);
    expect(readFileSync(outFile, 'utf-8')).toEqual(pdfBody);

    expect(cosRequests()).toEqual([{ method: 'GET', path: pdfPath }]);

    // The download negotiates a different media type than the JSON:API reads.
    const pdfRequest = server
      .getRequests()
      .find((req) => req.url?.includes('/report/pdf'));
    expect(pdfRequest?.headers.accept).toContain('application/pdf');
  });

  test('fails without --scan-id', async () => {
    const { code, stdout } = await runSnykCLI(
      `cos scan report --tenant-id=${tenantId}`,
      { env },
    );
    expect(code).toEqual(2);
    expect(stdout).toContain('--scan-id');
    expect(cosRequests()).toEqual([]);
  });
});

describe('cos target create', () => {
  test('creates a target from a config file', async () => {
    server.setEndpointResponse(targetsPath, {
      data: targetResource(),
      links: { self: `${targetsPath}/${targetId}` },
    });

    const { code, stdout } = await runSnykCLI(
      `cos target create --tenant-id=${tenantId} --config=${configFile}`,
      { env },
    );
    expect(code).toEqual(0);
    expect(stdout).toContain('Target created');
    expect(stdout).toContain(targetId);

    expect(cosRequests()).toEqual([{ method: 'POST', path: targetsPath }]);
  });

  test('fails when the config is missing target.url', async () => {
    const { code } = await runSnykCLI(
      `cos target create --tenant-id=${tenantId} --config=${invalidConfigFile}`,
      { env },
    );
    expect(code).toEqual(2);
    // Validation happens before any API call.
    expect(cosRequests()).toEqual([]);
  });
});

describe('cos target list', () => {
  test('lists targets in a table', async () => {
    server.setEndpointResponse(targetsPath, {
      data: [targetResource()],
      links: { self: targetsPath },
    });

    const { code, stdout } = await runSnykCLI(
      `cos target list --tenant-id=${tenantId}`,
      { env },
    );
    expect(code).toEqual(0);
    expect(stdout).toContain('ID');
    expect(stdout).toContain('NAME');
    expect(stdout).toContain(targetId);
    expect(stdout).toContain('Demo App');

    expect(cosRequests()).toEqual([{ method: 'GET', path: targetsPath }]);
  });

  test('handles an empty result', async () => {
    server.setEndpointResponse(targetsPath, {
      data: [],
      links: { self: targetsPath },
    });

    const { code, stdout } = await runSnykCLI(
      `cos target list --tenant-id=${tenantId}`,
      { env },
    );
    expect(code).toEqual(0);
    expect(stdout).toContain('No targets found.');
  });
});

describe('cos target get', () => {
  test('shows a target', async () => {
    server.setEndpointResponse(targetPath, {
      data: targetResource(),
      links: { self: targetPath },
    });

    const { code, stdout } = await runSnykCLI(
      `cos target get --tenant-id=${tenantId} --target-id=${targetId}`,
      { env },
    );
    expect(code).toEqual(0);
    expect(stdout).toContain(targetId);
    expect(stdout).toContain('Demo App');
    expect(stdout).toContain('https://demo-app.com');

    expect(cosRequests()).toEqual([{ method: 'GET', path: targetPath }]);
  });

  test('renders JSON with -o json', async () => {
    server.setEndpointResponse(targetPath, {
      data: targetResource(),
      links: { self: targetPath },
    });

    const { code, stdout } = await runSnykCLI(
      `cos target get --tenant-id=${tenantId} --target-id=${targetId} -o json`,
      { env },
    );
    expect(code).toEqual(0);

    const parsed = JSON.parse(stdout);
    expect(parsed).toMatchObject({
      id: targetId,
      name: 'Demo App',
      url: 'https://demo-app.com',
    });
  });

  test('fails without --target-id', async () => {
    const { code, stdout } = await runSnykCLI(
      `cos target get --tenant-id=${tenantId}`,
      { env },
    );
    expect(code).toEqual(2);
    expect(stdout).toContain('--target-id');
    expect(cosRequests()).toEqual([]);
  });
});

describe('cos target update', () => {
  test('updates a target from a config file', async () => {
    // One registration answers both requests: the endpoint config is keyed by
    // path, not by method.
    server.setEndpointResponse(targetPath, {
      data: targetResource(),
      links: { self: targetPath },
    });

    const { code, stdout } = await runSnykCLI(
      `cos target update --tenant-id=${tenantId} --target-id=${targetId} --config=${configFile}`,
      { env },
    );
    expect(code).toEqual(0);
    expect(stdout).toContain('Target updated');
    expect(stdout).toContain(targetId);

    expect(cosRequests()).toEqual([
      { method: 'GET', path: targetPath },
      { method: 'PATCH', path: targetPath },
    ]);
  });

  test('fails without --config', async () => {
    const { code, stdout } = await runSnykCLI(
      `cos target update --tenant-id=${tenantId} --target-id=${targetId}`,
      { env },
    );
    expect(code).toEqual(2);
    expect(stdout).toContain('--config');
    expect(cosRequests()).toEqual([]);
  });
});

describe('cos target delete', () => {
  test('deletes a target with --yes', async () => {
    server.setEndpointResponse(targetPath, {
      data: targetResource(),
      links: { self: targetPath },
    });

    const { code, stdout } = await runSnykCLI(
      `cos target delete --tenant-id=${tenantId} --target-id=${targetId} --yes`,
      { env },
    );
    expect(code).toEqual(0);
    expect(stdout).toContain(targetId);
    expect(stdout).toContain('deleted');

    expect(cosRequests()).toEqual([
      { method: 'GET', path: targetPath },
      { method: 'DELETE', path: targetPath },
    ]);
  });
});

describe('cos target dump', () => {
  test('prints the target configuration as YAML', async () => {
    server.setEndpointResponse(targetPath, {
      data: targetResource(),
      links: { self: targetPath },
    });

    const { code, stdout } = await runSnykCLI(
      `cos target dump --tenant-id=${tenantId} --target-id=${targetId}`,
      { env },
    );
    expect(code).toEqual(0);
    expect(stdout).toContain('target:');
    expect(stdout).toContain('name: Demo App');
    expect(stdout).toContain('url: https://demo-app.com');

    expect(cosRequests()).toEqual([{ method: 'GET', path: targetPath }]);
  });

  test('writes the YAML to --output-file', async () => {
    server.setEndpointResponse(targetPath, {
      data: targetResource(),
      links: { self: targetPath },
    });

    const outFile = join(makeTmpDir(), 'cos.yaml');

    const { code } = await runSnykCLI(
      `cos target dump --tenant-id=${tenantId} --target-id=${targetId} --output-file=${outFile}`,
      { env },
    );
    expect(code).toEqual(0);

    const written = readFileSync(outFile, 'utf-8');
    expect(written).toContain('name: Demo App');
    expect(written).toContain('url: https://demo-app.com');
  });
});

describe('cos finding list', () => {
  test('lists findings in a table', async () => {
    // The command resolves --target-id before listing, so the target lookup
    // has to answer as well.
    server.setEndpointResponse(targetPath, {
      data: targetResource(),
      links: { self: targetPath },
    });
    server.setEndpointResponse(findingsPath, {
      data: [findingResource()],
      links: { self: findingsPath },
    });

    const { code, stdout } = await runSnykCLI(
      `cos finding list --tenant-id=${tenantId} --target-id=${targetId}`,
      { env },
    );
    expect(code).toEqual(0);
    expect(stdout).toContain('ID');
    expect(stdout).toContain('SEVERITY');
    expect(stdout).toContain(findingId);
    expect(stdout).toContain('high');

    expect(cosRequests()).toEqual([
      { method: 'GET', path: targetPath },
      { method: 'GET', path: findingsPath },
    ]);
  });

  test('renders JSON with -o json', async () => {
    server.setEndpointResponse(targetPath, {
      data: targetResource(),
      links: { self: targetPath },
    });
    server.setEndpointResponse(findingsPath, {
      data: [findingResource()],
      links: { self: findingsPath },
    });

    const { code, stdout } = await runSnykCLI(
      `cos finding list --tenant-id=${tenantId} --target-id=${targetId} -o json`,
      { env },
    );
    expect(code).toEqual(0);

    const parsed = JSON.parse(stdout);
    expect(parsed.findings).toHaveLength(1);
    expect(parsed.findings[0]).toMatchObject({
      id: findingId,
      severity: 'high',
    });
  });

  test('fails without --target-id', async () => {
    const { code, stdout } = await runSnykCLI(
      `cos finding list --tenant-id=${tenantId}`,
      { env },
    );
    expect(code).toEqual(2);
    expect(stdout).toContain('--target-id');
    expect(cosRequests()).toEqual([]);
  });
});

describe('cos finding get', () => {
  test('shows the finding detail', async () => {
    server.setEndpointResponse(findingPath, {
      data: findingResource({
        description: 'The login form concatenates input into a SQL query.',
      }),
      links: { self: findingPath },
    });

    const { code, stdout } = await runSnykCLI(
      `cos finding get --tenant-id=${tenantId} --finding-id=${findingId}`,
      { env },
    );
    expect(code).toEqual(0);
    expect(stdout).toContain(findingId);
    expect(stdout).toContain('SQL Injection');
    expect(stdout).toContain('high');
    expect(stdout).toContain('/login');
    // Detail fields are rendered under their own headings.
    expect(stdout).toContain('Description');
    expect(stdout).toContain('concatenates input into a SQL query');

    expect(cosRequests()).toEqual([{ method: 'GET', path: findingPath }]);
  });

  test('renders JSON with --json', async () => {
    server.setEndpointResponse(findingPath, {
      data: findingResource(),
      links: { self: findingPath },
    });

    const { code, stdout } = await runSnykCLI(
      `cos finding get --tenant-id=${tenantId} --finding-id=${findingId} --json`,
      { env },
    );
    expect(code).toEqual(0);

    const parsed = JSON.parse(stdout);
    expect(parsed).toMatchObject({
      id: findingId,
      severity: 'high',
      title: 'SQL Injection',
    });
  });

  test('fails without --finding-id', async () => {
    const { code, stdout } = await runSnykCLI(
      `cos finding get --tenant-id=${tenantId}`,
      { env },
    );
    expect(code).toEqual(2);
    expect(stdout).toContain('--finding-id');
    expect(cosRequests()).toEqual([]);
  });
});

describe('error handling', () => {
  test('handles unauthenticated requests', async () => {
    const { code, stdout } = await runSnykCLI(
      `cos scan list --tenant-id=${tenantId}`,
      { env: envWithoutAuth },
    );
    expect(code).toEqual(2);
    expect(stdout).toContain('Authentication error (SNYK-0005)');
  });

  test('rejects the --org flag', async () => {
    const { code, stdout } = await runSnykCLI(
      `cos scan list --org=my-org`,
📚 Repository Context Analyzed

This review considered 12 relevant code sections from 7 files (average relevance: 0.97)

🤖 Repository instructions applied (from AGENTS.md)

@CatalinSnyk

CatalinSnyk commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

The spec file needs a format. And just an FYI there's this helper in config_utils if you wanted to accept the command with/without --experimental (not required, just wanted to let you guys know it's available if customers already integrated with --experimental).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants