Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
323ab3c
chore: drive acceptance skips from CircleCI context (CLI-1464)
robertolopezlopez May 13, 2026
4ae632b
test: migrate `createJestConfig` tests to TypeScript and refine imple…
robertolopezlopez May 13, 2026
6c9c505
chore: fix lint issues in acceptance tests
robertolopezlopez May 13, 2026
063f828
chore: add `acceptanceIt` to custom test block functions in ESLint co…
robertolopezlopez May 13, 2026
1a46906
chore: add TAP test output sanitation for CircleCI compatibility
robertolopezlopez May 13, 2026
707b886
test: ensure `createJestConfig` warns only once per execution
robertolopezlopez May 13, 2026
ae595fa
chore(circleci): update `yq` installation path to `/usr/bin`
robertolopezlopez May 13, 2026
7318c58
chore(circleci): update `yq` installation path to `$HOME/.local/bin` …
robertolopezlopez May 13, 2026
49e0a95
chore(circleci): remove redundant `yq` installation step
robertolopezlopez May 13, 2026
8dae1b8
chore(circleci): simplify TAP test command by removing redundant erro…
robertolopezlopez May 13, 2026
89de177
chore: migrate Jest configurations to TypeScript and enhance ignore f…
robertolopezlopez May 13, 2026
e2706b0
chore: remove TAP JUnit sanitation script and update observability lo…
robertolopezlopez May 13, 2026
1c2fe82
chore: migrate Jest configurations from TypeScript to JavaScript
robertolopezlopez May 13, 2026
dd483c4
chore: log skipped test IDs in `acceptanceIt` for improved test obser…
robertolopezlopez May 14, 2026
48dc353
chore: refactor and modularize Jest ignore list and skip tests handling
robertolopezlopez May 15, 2026
4a86a98
chore: migrate Jest configurations to TypeScript and reorganize `crea…
robertolopezlopez May 15, 2026
bb33721
chore: enhance `acceptanceIt` logging for skipped test IDs
robertolopezlopez May 15, 2026
6d36a3c
chore: simplify destructuring in `createJestConfig` for `testPathIgno…
robertolopezlopez May 15, 2026
b59044e
chore: add missing dependencies to `check-dependencies.config.ts` for…
robertolopezlopez May 15, 2026
c9536ab
Revert "chore: refactor and modularize Jest ignore list and skip test…
robertolopezlopez May 15, 2026
57d2a45
chore: revert TypeScript migration for `createJestConfig` and restore…
robertolopezlopez May 15, 2026
787cff7
chore: refactor ignore list and skip test handling with shared parser
robertolopezlopez May 19, 2026
cfdd7ef
chore: streamline variable naming and destructuring in `createJestCon…
robertolopezlopez May 19, 2026
e0b856a
chore: refactor handling of `TEST_SNYK_SKIP_TEST_IDS` for literal str…
robertolopezlopez May 20, 2026
dd8cc8e
test: clarify createJestConfig env setup in unit tests
robertolopezlopez May 20, 2026
6cab26b
test: add unit tests for `TEST_SNYK_IGNORE_LIST` in `createJestConfig`
robertolopezlopez May 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,7 @@ workflows:
- nodejs-install
- team_hammerhead-cli
- devex_cli_docker_hub
- team-cli-workflow-context
filters:
branches:
ignore:
Expand All @@ -785,6 +786,7 @@ workflows:
- nodejs-install
- team_hammerhead-cli
- devex_cli_docker_hub
- team-cli-workflow-context
filters:
branches:
ignore:
Expand All @@ -807,6 +809,7 @@ workflows:
- nodejs-install
- team_hammerhead-cli
- devex_cli_docker_hub
- team-cli-workflow-context
filters:
branches:
ignore:
Expand All @@ -832,6 +835,7 @@ workflows:
- nodejs-install
- team_hammerhead-cli
- devex_cli_docker_hub
- team-cli-workflow-context
filters:
branches:
ignore:
Expand All @@ -851,6 +855,7 @@ workflows:
context:
- nodejs-install
- team_hammerhead-cli
- team-cli-workflow-context
filters:
branches:
ignore:
Expand All @@ -874,6 +879,7 @@ workflows:
context:
- nodejs-install
- team_hammerhead-cli
- team-cli-workflow-context
filters:
branches:
ignore:
Expand All @@ -896,6 +902,7 @@ workflows:
context:
- nodejs-install
- team_hammerhead-cli
- team-cli-workflow-context
filters:
branches:
ignore:
Expand All @@ -916,6 +923,7 @@ workflows:
context:
- nodejs-install
- team_hammerhead-cli
- team-cli-workflow-context
filters:
branches:
ignore:
Expand Down
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@
"additionalTestBlockFunctions": [
"testIf",
"describeIf",
"testIf.each"
"testIf.each",
"acceptanceIt"
]
}
]
Expand Down
32 changes: 32 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,38 @@ You can run acceptance tests with:
npm run test:acceptance -- --selectProjects coreCli
```

#### Skipping acceptance spec files via CircleCI context (`TEST_SNYK_IGNORE_LIST`)

When needed (for example blocking failures outside the CLI), CI can exclude specific acceptance specs by path without changing repo code. Set the environment variable **`TEST_SNYK_IGNORE_LIST`** to a comma-separated list of **regex fragments** that Jest merges into `testPathIgnorePatterns` (same semantics as Jest’s ignore patterns). Empty entries are ignored after trimming. Each fragment must compile as a JavaScript **`RegExp`** source; malformed fragments are skipped and listed on stderr under **`[acceptance ignore]`** so Jest still starts.

- **CircleCI:** add the variable on context **`team-cli-workflow-context`** (name `TEST_SNYK_IGNORE_LIST`, value is only the pattern text—for example `snyk-code-user-journey\.spec\.ts`—not `TEST_SNYK_IGNORE_LIST=...`). Those workflows attach that context to **`acceptance-tests`** jobs so the env is available there.
- **Precedence:** for paths that match a fragment, **`TEST_SNYK_IGNORE_LIST` wins over `TEST_SNYK_DONT_SKIP_ANYTHING`** (the file is not collected). `TEST_SNYK_DONT_SKIP_ANYTHING` still applies to specs that remain in the run.
- **Observability:** **`console.warn`** on **stderr** with prefix **`[acceptance ignore]`**: skipped invalid fragments (if any), then applied fragments and precedence vs **`TEST_SNYK_DONT_SKIP_ANYTHING`**—each summary logged once per worker; avoid relying on stdout for this signal.

`testPathIgnorePatterns` applies to whole files; it cannot skip a single `it()` inside a spec.

#### Skipping individual acceptance tests via CircleCI context (`TEST_SNYK_SKIP_TEST_IDS`)

CI can skip selected `it()` blocks inside specs that Jest still collects, using comma-separated **stable ids** (empty entries ignored after trimming). **Ids must not contain commas.**

- **CircleCI:** add the variable on context **`team-cli-workflow-context`** (name `TEST_SNYK_SKIP_TEST_IDS`, value is only the id list—for example `snyk-code-user-journey:golang-native:ignored-issues:severity-threshold,snyk-code-user-journey:golang-native:ignored-issues:single-file`—not `TEST_SNYK_SKIP_TEST_IDS=...`). Those workflows attach that context to **`acceptance-tests`** jobs so the env is available there.
- **Wiring:** specs use **`acceptanceIt(id)`** from [`test/jest/util/acceptanceTestSkipById.ts`](test/jest/util/acceptanceTestSkipById.ts); stable ids for the Snyk Code user-journey are **`SnykCodeUserJourneyContextSkipIds`** in that file.
- **Precedence:** if a path matches **`TEST_SNYK_IGNORE_LIST`**, Jest never collects that spec—**`TEST_SNYK_SKIP_TEST_IDS` has no effect** on tests inside it. **`TEST_SNYK_DONT_SKIP_ANYTHING`** does **not** unsuspend **`acceptanceIt`** skips; clear **`TEST_SNYK_SKIP_TEST_IDS`** instead.
- **Observability:** **`console.warn`** on **stderr** with prefix **`[acceptance skip tests]`**, listing ids—once per worker; avoid relying on stdout for this signal.

Stable ids for the golang/native ignored-issues journeys:

- `snyk-code-user-journey:golang-native:ignored-issues:severity-threshold`
- `snyk-code-user-journey:golang-native:ignored-issues:include-ignores`
- `snyk-code-user-journey:golang-native:ignored-issues:single-file`

Local repro:

```
TEST_SNYK_SKIP_TEST_IDS='snyk-code-user-journey:golang-native:ignored-issues:severity-threshold' \
npm run test:acceptance -- --selectProjects coreCli --runTestsByPath test/jest/acceptance/snyk-code/snyk-code-user-journey.spec.ts
```

### Smoke Tests

Smoke tests typically don't run on branches unless the branch is specifically prefixed with `smoke/`. They usually run
Expand Down
84 changes: 81 additions & 3 deletions test/createJestConfig.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,55 @@
const createJestConfig = (config) => {
/**
* Comma-separated env list: trim segments, drop empties. For literal string sets (no RegExp checks).
* @param {string} raw
* @returns {string[]}
*/
function parseCommaSeparatedEnvList(raw) {
if (typeof raw !== 'string' || raw.trim() === '') {
return [];
}
return raw
.split(',')
.map((s) => s.trim())
.filter(Boolean);
}

/**
* Comma-separated env list (`TEST_SNYK_IGNORE_LIST`): trim, drop empties,
* validate each fragment as a JavaScript RegExp source (same rule as Jest path patterns).
* @param {string} raw
* @returns {{ valid: string[], invalid: string[] }}
*/
function parseSnykIgnoreFragments(raw) {
if (typeof raw !== 'string' || raw.trim() === '') {
return { valid: [], invalid: [] };
}
const pieces = raw
.split(',')
.map((s) => s.trim())
.filter(Boolean);
const valid = [];
const invalid = [];
for (const f of pieces) {
try {
// Validate fragment as a JavaScript RegExp source (same as Jest path patterns).
// eslint-disable-next-line no-new
new RegExp(f);
valid.push(f);
} catch {
invalid.push(f);
}
}
return { valid, invalid };
}

function getSkipTestList() {
const raw = process.env.TEST_SNYK_IGNORE_LIST;
return parseSnykIgnoreFragments(typeof raw === 'string' ? raw : '');
}

let ignoreFragmentsWarned = false;

const createJestConfig = (config = {}) => {
const ignorePatterns = [
'/node_modules/',
'/dist/',
Expand All @@ -9,17 +60,44 @@ const createJestConfig = (config) => {
'<rootDir>/pysrc/',
];

const { valid, invalid } = getSkipTestList();

if ((valid.length > 0 || invalid.length > 0) && !ignoreFragmentsWarned) {
ignoreFragmentsWarned = true;
if (invalid.length > 0) {
console.warn(
'[acceptance ignore]',
'Skipping invalid TEST_SNYK_IGNORE_LIST fragments (must compile as JavaScript RegExp sources):',
invalid,
);
}
if (valid.length > 0) {
console.warn(
'[acceptance ignore]',
valid,
'TEST_SNYK_IGNORE_LIST overrides TEST_SNYK_DONT_SKIP_ANYTHING for matching files.',
);
}
}

const { testPathIgnorePatterns, ...restConfig } = config;
const extraPathIgnores = Array.isArray(testPathIgnorePatterns)
? testPathIgnorePatterns
: [];

return {
preset: 'ts-jest',
testRegex: '\\.spec\\.ts$',
testPathIgnorePatterns: [...ignorePatterns],
testPathIgnorePatterns: [...ignorePatterns, ...valid, ...extraPathIgnores],
modulePathIgnorePatterns: [...ignorePatterns],
coveragePathIgnorePatterns: [...ignorePatterns],
transformIgnorePatterns: [...ignorePatterns],
...config,
...restConfig,
};
};

module.exports = {
createJestConfig,
parseCommaSeparatedEnvList,
parseSnykIgnoreFragments,
};
49 changes: 49 additions & 0 deletions test/jest/acceptance/resilience.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,55 @@ interface ResilienceScenario {
skip?: string[]; // Commands to skip for this scenario (not yet consistent)
}

describe('TEST_SNYK_IGNORE_LIST → testPathIgnorePatterns', () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { createJestConfig } = require('../../createJestConfig') as {
createJestConfig: (config?: object) => {
testPathIgnorePatterns: string[];
};
};
let previousIgnoreList: string | undefined;

beforeEach(() => {
previousIgnoreList = process.env.TEST_SNYK_IGNORE_LIST;
});

afterEach(() => {
if (previousIgnoreList === undefined) {
delete process.env.TEST_SNYK_IGNORE_LIST;
} else {
process.env.TEST_SNYK_IGNORE_LIST = previousIgnoreList;
}
});

it('when unset or empty, does not add fragments from TEST_SNYK_IGNORE_LIST', () => {
for (const value of [undefined as string | undefined, '']) {
if (value === undefined) {
delete process.env.TEST_SNYK_IGNORE_LIST;
} else {
process.env.TEST_SNYK_IGNORE_LIST = value;
}
const ignorePathPatterns = createJestConfig({}).testPathIgnorePatterns;
expect(ignorePathPatterns).toContain('/node_modules/');
expect(ignorePathPatterns).not.toContain('happy-path-one');
}
});

it('single comma-separated pattern is merged', () => {
process.env.TEST_SNYK_IGNORE_LIST = 'happy-path-one';
expect(createJestConfig({}).testPathIgnorePatterns).toContain(
'happy-path-one',
);
});

it('two comma-separated patterns are merged', () => {
process.env.TEST_SNYK_IGNORE_LIST = 'happy-path-a, happy-path-b';
const ignorePathPatterns = createJestConfig({}).testPathIgnorePatterns;
expect(ignorePathPatterns).toContain('happy-path-a');
expect(ignorePathPatterns).toContain('happy-path-b');
});
});

const RESILIENCE_SCENARIOS: ResilienceScenario[] = [
// Scenario 1
{
Expand Down
19 changes: 14 additions & 5 deletions test/jest/acceptance/snyk-code/snyk-code-user-journey.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import {
createFilepaths,
deleteFilepaths,
} from '../../../jest/util/fileIgnoreRulesFixture';
import {
acceptanceIt,
SnykCodeUserJourneyContextSkipIds,
} from '../../util/acceptanceTestSkipById';

expect.extend(matchers);
jest.setTimeout(1000 * 300);
Expand Down Expand Up @@ -774,8 +778,10 @@ describe('snyk code test', () => {
}
});

// TODO: Temporarily skipped - investigate persistent CI failures
it.skip('with --severity-threshold', async () => {
// `TEST_SNYK_SKIP_TEST_IDS` (CircleCI context) → `acceptanceIt(<id>)`
acceptanceIt(
SnykCodeUserJourneyContextSkipIds.GOLANG_NATIVE_IGNORED_ISSUES_SEVERITY_THRESHOLD,
)('with --severity-threshold', async () => {
const { stdout, stderr, code } = await runSnykCLI(
`code test ${pathToTest} --severity-threshold=high --sarif-file-output=${sarifFile}`,
{
Expand Down Expand Up @@ -805,8 +811,9 @@ describe('snyk code test', () => {
expect(levels.length).toBe(0);
});

// TODO: Temporarily skipped - investigate persistent CI failures
it.skip('with --include-ignores', async () => {
acceptanceIt(
SnykCodeUserJourneyContextSkipIds.GOLANG_NATIVE_IGNORED_ISSUES_INCLUDE_IGNORES,
)('with --include-ignores', async () => {
const { stdout, stderr, code } = await runSnykCLI(
`code test ${pathToTest} --include-ignores --sarif-file-output=${sarifFile}`,
{
Expand All @@ -831,7 +838,9 @@ describe('snyk code test', () => {
);

describe(`with ignored issues`, () => {
it.skip('test a single file', async () => {
acceptanceIt(
SnykCodeUserJourneyContextSkipIds.GOLANG_NATIVE_IGNORED_ISSUES_SINGLE_FILE,
)('test a single file', async () => {
const { stderr, code } = await runSnykCLI(
`code test ${localPath}/routes/index.js --sarif-file-output=${sarifFile}`,
{
Expand Down
78 changes: 78 additions & 0 deletions test/jest/unit/acceptanceTestSkipById.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
describe('acceptanceTestSkipById (TEST_SNYK_SKIP_TEST_IDS)', () => {
afterEach(() => {
delete process.env.TEST_SNYK_SKIP_TEST_IDS;
jest.resetModules();
jest.restoreAllMocks();
});

async function loadModule() {
jest.resetModules();
return import('../util/acceptanceTestSkipById');
}

it('returns global `it` when TEST_SNYK_SKIP_TEST_IDS is unset', async () => {
delete process.env.TEST_SNYK_SKIP_TEST_IDS;
const { acceptanceIt } = await loadModule();
expect(acceptanceIt('any-stable-id')).toBe(it);
});

it('returns global `it` when TEST_SNYK_SKIP_TEST_IDS is empty or whitespace-only', async () => {
process.env.TEST_SNYK_SKIP_TEST_IDS = ' ';
const { acceptanceIt } = await loadModule();
expect(acceptanceIt('any-stable-id')).toBe(it);
});

it('returns `it.skip` when the id is listed', async () => {
jest.spyOn(console, 'warn').mockImplementation(() => {});
process.env.TEST_SNYK_SKIP_TEST_IDS = 'stable-id-one';
const { acceptanceIt } = await loadModule();
expect(acceptanceIt('stable-id-one')).toBe(it.skip);
expect(acceptanceIt('other-id')).toBe(it);
});

it('parses comma-separated ids with trimming and ignores empty segments', async () => {
jest.spyOn(console, 'warn').mockImplementation(() => {});
process.env.TEST_SNYK_SKIP_TEST_IDS = ' a , , b ';
const { acceptanceIt } = await loadModule();
expect(acceptanceIt('a')).toBe(it.skip);
expect(acceptanceIt('b')).toBe(it.skip);
expect(acceptanceIt('c')).toBe(it);
});

it('treats stable ids as literal strings, not RegExp sources (e.g. `[` in the id)', async () => {
jest.spyOn(console, 'warn').mockImplementation(() => {});
const id = 'suite:case:with[bracket-unclosed';
process.env.TEST_SNYK_SKIP_TEST_IDS = id;
const { acceptanceIt } = await loadModule();
expect(acceptanceIt(id)).toBe(it.skip);
expect(acceptanceIt('other-id')).toBe(it);
});

it('logs [acceptance skip tests] once per module load when ids are present', async () => {
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
process.env.TEST_SNYK_SKIP_TEST_IDS = 'x,y';
const { acceptanceIt } = await loadModule();
acceptanceIt('x');
acceptanceIt('y');
expect(warn).toHaveBeenCalledTimes(1);
expect(warn.mock.calls[0][0]).toBe('[acceptance skip tests]');
expect(warn.mock.calls[0][1]).toEqual(['x', 'y']);
});

it('exports documented stable id constants for the Snyk Code user journey', async () => {
const { SnykCodeUserJourneyContextSkipIds } = await loadModule();
expect(
SnykCodeUserJourneyContextSkipIds.GOLANG_NATIVE_IGNORED_ISSUES_SEVERITY_THRESHOLD,
).toBe(
'snyk-code-user-journey:golang-native:ignored-issues:severity-threshold',
);
expect(
SnykCodeUserJourneyContextSkipIds.GOLANG_NATIVE_IGNORED_ISSUES_INCLUDE_IGNORES,
).toBe(
'snyk-code-user-journey:golang-native:ignored-issues:include-ignores',
);
expect(
SnykCodeUserJourneyContextSkipIds.GOLANG_NATIVE_IGNORED_ISSUES_SINGLE_FILE,
).toBe('snyk-code-user-journey:golang-native:ignored-issues:single-file');
});
});
Loading
Loading