Skip to content

Commit bdd25b8

Browse files
committed
Use system status handler
1 parent 2b40989 commit bdd25b8

4 files changed

Lines changed: 159 additions & 64 deletions

File tree

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
name: Post-Beta Long Running Tests
2+
run-name: Long Running Tests for ${{ github.event.release.tag_name }}
3+
4+
on:
5+
release:
6+
types: [published]
7+
8+
permissions:
9+
contents: read
10+
11+
jobs:
12+
check-beta-release:
13+
runs-on: ubuntu-latest
14+
outputs:
15+
is-beta: ${{ steps.check.outputs.is-beta }}
16+
steps:
17+
- name: Check if beta release
18+
id: check
19+
run: |
20+
TAG="${{ github.event.release.tag_name }}"
21+
if [[ "$TAG" =~ -beta$ ]]; then
22+
echo "is-beta=true" >> $GITHUB_OUTPUT
23+
else
24+
echo "is-beta=false" >> $GITHUB_OUTPUT
25+
fi
26+
27+
long-running-tests:
28+
needs: check-beta-release
29+
if: needs.check-beta-release.outputs.is-beta == 'true'
30+
strategy:
31+
matrix:
32+
os: [ubuntu-latest, windows-latest, macos-latest]
33+
node-version: [18, 20, 22]
34+
runs-on: ${{ matrix.os }}
35+
timeout-minutes: 360
36+
steps:
37+
- uses: actions/checkout@v5
38+
with:
39+
ref: ${{ github.event.release.tag_name }}
40+
41+
- name: Setup Node.js
42+
uses: actions/setup-node@v4
43+
with:
44+
node-version: ${{ matrix.node-version }}
45+
46+
- name: Download release standalone
47+
env:
48+
GH_TOKEN: ${{ github.token }}
49+
run: |
50+
TAG="${{ github.event.release.tag_name }}"
51+
case "${{ matrix.os }}" in
52+
ubuntu-latest) PLATFORM="linux" ;;
53+
windows-latest) PLATFORM="win32" ;;
54+
macos-latest) PLATFORM="darwin" ;;
55+
esac
56+
PATTERN="*${PLATFORM}*x64*node${{ matrix.node-version }}*.zip"
57+
gh release download "$TAG" --pattern "$PATTERN"
58+
59+
- name: Extract standalone bundle
60+
run: |
61+
if [[ "${{ matrix.os }}" == "windows-latest" ]]; then
62+
unzip *.zip
63+
else
64+
unzip *.zip
65+
chmod +x cfn-lsp-server-standalone.js
66+
fi
67+
68+
- name: Test standalone bundle
69+
run: node cfn-lsp-server-standalone.js --version
70+
71+
- name: Install dependencies
72+
run: npm ci
73+
74+
- name: Run long-running stability tests
75+
env:
76+
STABILITY_TEST_DURATION: 4h
77+
STANDALONE_PATH: ./cfn-lsp-server-standalone.js
78+
run: npm run test:stability
79+
80+
- name: Upload test results
81+
if: always()
82+
uses: actions/upload-artifact@v4
83+
with:
84+
name: long-running-test-results-${{ matrix.os }}
85+
path: |
86+
test-results.json
87+
test-logs.txt
88+
if-no-files-found: ignore

tools/lspClient/LspClient.ts

Lines changed: 14 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import {
1111
} from 'vscode-languageserver-protocol/node';
1212
import { randomBytes } from 'crypto';
1313
import { CompactEncrypt } from 'jose';
14-
import { LspClientConfig, LspConnection, InitializationFlags } from './LspConnection';
14+
import { LspClientConfig, LspConnection } from './LspConnection';
1515
import { ExtendedInitializeParams } from '../../src/server/InitParams';
1616
import { IamCredentials } from '../../src/auth/AwsLspAuthTypes';
17+
import { GetSystemStatusResponse } from '../../src/protocol/LspSystemHandlers';
1718
import { WaitFor } from '../../tst/utils/Utils';
1819

1920
/**
@@ -23,16 +24,11 @@ import { WaitFor } from '../../tst/utils/Utils';
2324
export class LspClient implements LspConnection {
2425
protected serverProcess?: ChildProcess;
2526
protected connection?: MessageConnection;
26-
protected initialization: InitializationFlags = {
27-
cfnLint: false,
28-
cfnGuard: false,
29-
};
3027

3128
public readonly createdAt: number;
3229
private readonly encryptionKey: Buffer;
3330
protected isShutdown = false;
3431
protected workspaceConfig: Record<string, unknown>[] = [{}];
35-
protected availableRegions = new Set<string>();
3632

3733
constructor(protected config: LspClientConfig) {
3834
this.createdAt = performance.now();
@@ -105,21 +101,7 @@ export class LspClient implements LspConnection {
105101
private readonly onServerOutput = (data: Buffer) => {
106102
const output = data.toString().trim();
107103

108-
// external service initialization detection
109-
if (output.includes('cfn-lint version')) {
110-
this.initialization.cfnLint = true;
111-
}
112-
if (output.includes('Loading rules from')) {
113-
this.initialization.cfnGuard = true;
114-
}
115-
116-
// Region-specific schema loading
117-
const regionSchemaMatch = output.match(/public schemas downloaded for ([a-z0-9-]+)/);
118-
if (regionSchemaMatch) {
119-
this.availableRegions.add(regionSchemaMatch[1]);
120-
}
121-
122-
// Log filtering
104+
// Log filtering - keep for debugging
123105
const suppressLevels = this.config.suppressLogLevels ?? ['INFO', 'DEBUG'];
124106
const shouldSuppress = suppressLevels.some((level) => output.includes(`${level}:`));
125107

@@ -268,11 +250,15 @@ export class LspClient implements LspConnection {
268250
}
269251

270252
async waitForExternalServiceInitialization(): Promise<void> {
271-
console.log('Waiting for lint and guard initialization');
253+
console.log('Waiting for lint and guard initialization via SystemHandler...');
272254

273255
await WaitFor.waitFor(
274-
() => {
275-
if (!this.initialization.cfnLint || !this.initialization.cfnGuard) {
256+
async () => {
257+
const status = await this.getSystemStatus();
258+
console.log(
259+
`Service status: cfnLint=${status.cfnLintReady.ready}, cfnGuard=${status.cfnGuardReady.ready}`,
260+
);
261+
if (!status.cfnLintReady.ready || !status.cfnGuardReady.ready) {
276262
throw new Error('Lint and Guard services not initialized');
277263
}
278264
console.log('Lint and Guard services are initialized');
@@ -282,10 +268,6 @@ export class LspClient implements LspConnection {
282268
);
283269
}
284270

285-
getAvailableRegions(): ReadonlySet<string> {
286-
return this.availableRegions;
287-
}
288-
289271
async updateCredentials(credentials: IamCredentials): Promise<void> {
290272
const payload = new TextEncoder().encode(JSON.stringify({ data: credentials }));
291273
const jwt = await new CompactEncrypt(payload)
@@ -298,6 +280,10 @@ export class LspClient implements LspConnection {
298280
});
299281
}
300282

283+
async getSystemStatus(): Promise<GetSystemStatusResponse> {
284+
return await this.sendRequest('aws/system/status', {});
285+
}
286+
301287
async shutdown(): Promise<void> {
302288
if (this.isShutdown) return;
303289
this.isShutdown = true;

tools/lspClient/LspConnection.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,3 @@ export type LspClientConfig = {
2121
env?: NodeJS.ProcessEnv;
2222
suppressLogLevels?: string[];
2323
};
24-
25-
export type InitializationFlags = {
26-
cfnLint: boolean;
27-
cfnGuard: boolean;
28-
};

tools/stability/TestOrchestrator.ts

Lines changed: 57 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ export class TestOrchestrator {
1818

1919
private readonly templates = TEMPLATE_CONFIGS;
2020

21-
private readonly testRegions = Object.values(AwsRegion);
21+
private readonly testRegions = Object.values(AwsRegion).filter(
22+
(region) => region !== AwsRegion.ME_SOUTH_1 && region !== AwsRegion.ME_CENTRAL_1,
23+
);
2224

2325
async initialize(): Promise<void> {
2426
console.log('Starting CloudFormation Language Server Long-Running Tests');
@@ -58,7 +60,24 @@ export class TestOrchestrator {
5860

5961
console.log(`Loaded ${this.templates.length} templates`);
6062

61-
await this.client.waitForExternalServiceInitialization();
63+
// Wait for all system components to be ready
64+
console.log('Waiting for system components to be ready...');
65+
await WaitFor.waitFor(
66+
async () => {
67+
const status = await this.client.getSystemStatus();
68+
if (
69+
!status.settingsReady.ready ||
70+
!status.schemasReady.ready ||
71+
!status.cfnLintReady.ready ||
72+
!status.cfnGuardReady.ready
73+
) {
74+
throw new Error('System not ready');
75+
}
76+
},
77+
30_000,
78+
1000,
79+
);
80+
console.log('All system components ready');
6281

6382
await this.loadAllRegionSchemas();
6483

@@ -140,42 +159,32 @@ export class TestOrchestrator {
140159
}
141160

142161
private async loadAllRegionSchemas(): Promise<void> {
143-
// Check what regions are already available from LspClient
144-
const alreadyAvailable = [...this.client.getAvailableRegions()];
145-
const unavailableRegions = this.testRegions.filter((region) => !this.client.getAvailableRegions().has(region));
146-
147-
console.log(`Schema status: ${alreadyAvailable.length} available, ${unavailableRegions.length} unavailable`);
148-
console.log(`Available region schemas: ${alreadyAvailable.join(', ')}`);
149-
150-
if (unavailableRegions.length > 0) {
151-
console.log(`Loading the following region schemas: ${unavailableRegions.join(', ')}`);
152-
}
162+
console.log('Loading schemas for all regions...');
153163

154-
for (const region of unavailableRegions) {
164+
for (const region of this.testRegions) {
155165
await this.switchToRegion(region);
156-
await this.waitForRegionSchemas(region);
166+
167+
// Wait for schemas to be ready after region switch
168+
try {
169+
await WaitFor.waitFor(
170+
async () => {
171+
const status = await this.client.getSystemStatus();
172+
if (!status.schemasReady.ready) {
173+
throw new Error(`Schemas not ready for region ${region}`);
174+
}
175+
},
176+
30_000,
177+
200,
178+
);
179+
} catch (error) {
180+
console.warn(`Failed to load schemas for region ${region}, continuing anyway:`, error);
181+
}
157182
}
158183

159184
console.log('Regional schema loading complete');
160185
}
161186

162-
private async waitForRegionSchemas(region: string): Promise<void> {
163-
try {
164-
await WaitFor.waitFor(
165-
() => {
166-
if (!this.client.getAvailableRegions().has(region)) {
167-
throw new Error(`Region ${region} schemas not loaded yet`);
168-
}
169-
},
170-
30_000, // 30 second timeout
171-
500, // Check every 500ms
172-
);
173-
} catch {
174-
console.warn(`Timeout waiting for ${region} schemas, proceeding anyway`);
175-
}
176-
}
177-
178-
private async switchToRegion(region: string): Promise<void> {
187+
private async switchToRegion(region: AwsRegion): Promise<void> {
179188
// Store the new configuration
180189
await this.client.changeConfiguration({
181190
settings: {
@@ -186,6 +195,23 @@ export class TestOrchestrator {
186195
},
187196
},
188197
});
198+
199+
// Wait for settings to be applied with correct region
200+
await WaitFor.waitFor(
201+
async () => {
202+
const status = await this.client.getSystemStatus();
203+
if (!status.settingsReady.ready) {
204+
throw new Error('Settings not ready after region change');
205+
}
206+
if (status.currentSettings.profile.region !== region) {
207+
throw new Error(
208+
`Region not applied: expected ${region}, got ${status.currentSettings.profile.region}`,
209+
);
210+
}
211+
},
212+
5000,
213+
100,
214+
); // Reduced timeout and faster polling
189215
}
190216

191217
private async validateLsp(uri: string): Promise<void> {

0 commit comments

Comments
 (0)