Skip to content

Commit 47bf4c1

Browse files
SakshiKoli-CSclaude
andcommitted
test(integration): add end-to-end test suite for launch-cli commands
Adds test/integration/ - a harness that runs real oclif commands end to end, plus 31 specs over launch (GitHub + FileUpload), environments, deployments, open and rollback. Assertions are on what the CLI does: requests sent, prompts asked, config file written, stdout and exit code. A refactor passes; a behaviour change fails with a readable diff. Also wires test:integration into npm test, narrows the mocha glob to test/unit/** so mocha stops picking up the jest specs, and adds the eslint and talisman entries the new files need. Refs CL-7154. No production code changed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 6755575 commit 47bf4c1

22 files changed

Lines changed: 2182 additions & 1 deletion

‎.talismanrc‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,16 @@ fileignoreconfig:
2626
- "headers\\?\\.reduce\\(\\(acc, \\{ key, value \\}\\)"
2727
- "\\(\\{ key, value \\}\\) => \\(\\{ key, value \\}\\)\\)"
2828
- "const \\{ token, apiKey \\} = configHandler\\.get"
29+
- filename: test/integration/specs/environments.test.ts
30+
checksum: f02e76dc6f4dfac2bc21d53357d70fc2b93c6473b9e71ab08300670f2c5649c1
31+
- filename: test/integration/jest.setup-env.ts
32+
checksum: eede2df0cdbce975a09ea96ed705db42fc9d6c3bdd36266a9be198653957b539
33+
- filename: test/integration/harness/launch-api.ts
34+
checksum: a4549b87c896744422e71ab5395a1d6d2dfa372678d2488aecc4247ee9738f87
35+
- filename: test/integration/harness/session.ts
36+
checksum: 74dc637304710b1df882bea6a1c52b8779f1bd416dbeeab93a9026c68edb3f9e
37+
- filename: test/integration/specs/launch-github.test.ts
38+
checksum: 4b9a39461dc09aa405b66fd3a17b660038c8ca7e6c0446166f938dff3faf0373
39+
- filename: test/integration/harness/fixtures.ts
40+
checksum: b3133a65f3f4b3f3a32caf0cc0bf65b0c71187f96e044a3331ddcce3adea88c1
2941
version: "1.0"

‎AGENTS.md‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,28 @@
88
- Use only jest for writing test cases and refer existing unit test under the /src folder.
99
- Do not create code comments for any changes.
1010

11+
## Integration tests
12+
13+
- Unit tests mock the CLI's own classes, so they keep passing when behaviour changes.
14+
Integration tests (`test/integration/`) exist to catch what unit tests cannot: they run a
15+
real oclif command end to end and assert on what the CLI *does*.
16+
- Run them with `npm run test:integration` (they are also part of `npm test`).
17+
- Only three things are replaced: the terminal prompt (`inquirer`), the `open` package, and
18+
the Contentstack management SDK, which resolves its base URL as `https://<host>:443` and so
19+
cannot be pointed at a local server. Apollo, the GraphQL documents, the CLI config store,
20+
flag parsing, the filesystem and `git` all run for real.
21+
- The Launch API is a local HTTP server (`test/integration/harness/launch-api.ts`) that the CLI
22+
reaches through a temporary region config. It records every GraphQL operation, so tests assert
23+
on the operations and variables the CLI actually sent rather than on internal method calls.
24+
- Assert on the observable contract - operation sequence, request variables, prompt sequence,
25+
`.cs-launch.json` contents, stdout, exit code. Never stub a method on a command or adapter;
26+
that is what makes these tests survive a refactor and fail on a behaviour change.
27+
- Register a response for every operation a flow needs and finish with
28+
`session.api.assertNoUnhandledOperations()` and `prompts.assertScriptFullyConsumed()`, so a
29+
renamed operation or an added prompt fails loudly instead of passing quietly.
30+
- V2 (epic CL-4867) removes the bare `csdx launch` command in favour of `projects:create`,
31+
`environments:create` and `deployments:create`. `launch-github.test.ts` and
32+
`launch-file-upload.test.ts` therefore describe behaviour that has to be *ported* to those
33+
commands, not a command that survives — read a failure there as "the new command does something
34+
different", and repoint the spec rather than deleting the assertion.
35+
- Tests named after a `CL-` ticket pin a bug that was already fixed once. Do not relax them.

‎eslint.config.mjs‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,21 @@ export default tseslint.config(
4040
// test files use @ts-nocheck and scaffolding vars that aren't all referenced
4141
'@typescript-eslint/ban-ts-comment': 'off',
4242
'@typescript-eslint/no-unused-vars': 'off',
43+
// harness mirrors untyped CLI payloads, and jest.mock factories must use require()
44+
'@typescript-eslint/no-explicit-any': 'off',
45+
'@typescript-eslint/no-require-imports': 'off',
46+
},
47+
},
48+
{
49+
files: ['*.config.js'],
50+
languageOptions: {
51+
sourceType: 'commonjs',
52+
globals: {
53+
module: 'writable',
54+
require: 'readonly',
55+
process: 'readonly',
56+
__dirname: 'readonly',
57+
},
4358
},
4459
},
4560
);

‎jest.integration.config.js‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const config = {
2+
testEnvironment: 'node',
3+
rootDir: '.',
4+
testMatch: ['<rootDir>/test/integration/specs/**/*.test.ts'],
5+
transform: {
6+
'^.+\\.ts$': ['ts-jest', { tsconfig: '<rootDir>/test/integration/tsconfig.json' }],
7+
'^.+\\.js$': [
8+
'ts-jest',
9+
{ tsconfig: { allowJs: true, module: 'commonjs', target: 'es2020', esModuleInterop: true } },
10+
],
11+
},
12+
transformIgnorePatterns: ['/node_modules/(?!uuid/)'],
13+
setupFiles: ['<rootDir>/test/integration/jest.setup-env.ts'],
14+
setupFilesAfterEnv: ['<rootDir>/test/integration/jest.setup.ts'],
15+
maxWorkers: 1,
16+
testTimeout: 30000,
17+
forceExit: true,
18+
coverageDirectory: 'coverage/integration',
19+
coverageProvider: 'v8',
20+
collectCoverageFrom: ['src/**/*.ts', '!src/test/**', '!src/**/*.test.ts'],
21+
};
22+
23+
module.exports = config;

‎package.json‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@
9191
"lint-staged": "lint-staged",
9292
"postpack": "shx rm -f oclif.manifest.json",
9393
"prepack-bkp": "npm run build && oclif manifest && oclif readme",
94-
"test": "mocha --forbid-only \"test/**/*.test.ts\" && npm run test:jest",
94+
"test": "mocha --forbid-only \"test/unit/**/*.test.ts\" && npm run test:jest && npm run test:integration",
9595
"version": "oclif readme && git add README.md",
9696
"build": "npm run clean && npm run compile",
9797
"clean": "rm -rf ./dist tsconfig.build.tsbuildinfo",
@@ -101,6 +101,8 @@
101101
"test:unit": "mocha --forbid-only \"test/unit/**/*.test.ts\" && npm run test:jest",
102102
"test:unit:report": "nyc --extension .ts mocha --forbid-only \"test/unit/**/*.test.ts\" && npm run test:jest:coverage",
103103
"test:jest": "jest",
104+
"test:integration": "jest --config jest.integration.config.js",
105+
"test:integration:coverage": "jest --config jest.integration.config.js --coverage",
104106
"test:jest:coverage": "jest --coverage",
105107
"prepare": "husky"
106108
},

‎test/integration/harness/cma.ts‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
export type MockOrganization = { uid: string; name: string };
2+
3+
export type RecordedCmaCall = {
4+
method: 'organization.fetch' | 'organization.fetchAll';
5+
uid?: string;
6+
params?: Record<string, any>;
7+
};
8+
9+
class CmaMock {
10+
private organizations: MockOrganization[] = [];
11+
private pageSize = 100;
12+
private recorded: RecordedCmaCall[] = [];
13+
14+
reset(): void {
15+
this.organizations = [];
16+
this.pageSize = 100;
17+
this.recorded = [];
18+
}
19+
20+
withOrganizations(organizations: MockOrganization[], pageSize = 100): this {
21+
this.organizations = organizations;
22+
this.pageSize = pageSize;
23+
return this;
24+
}
25+
26+
calls(): RecordedCmaCall[] {
27+
return [...this.recorded];
28+
}
29+
30+
client(): Record<string, any> {
31+
return {
32+
organization: (uid?: string) => ({
33+
fetch: async () => {
34+
this.recorded.push({ method: 'organization.fetch', uid });
35+
const organization = this.organizations.find((candidate) => candidate.uid === uid);
36+
if (!organization) throw new Error(`Organization "${uid}" not found`);
37+
return organization;
38+
},
39+
fetchAll: async (params: Record<string, any> = {}) => {
40+
this.recorded.push({ method: 'organization.fetchAll', params });
41+
const skip = Number(params.skip ?? 0);
42+
return {
43+
items: this.organizations.slice(skip, skip + this.pageSize),
44+
count: this.organizations.length,
45+
};
46+
},
47+
}),
48+
};
49+
}
50+
}
51+
52+
export const cma = new CmaMock();

0 commit comments

Comments
 (0)