diff --git a/src/handlers/StackHandler.ts b/src/handlers/StackHandler.ts index e9daa5ab..ff11d82b 100644 --- a/src/handlers/StackHandler.ts +++ b/src/handlers/StackHandler.ts @@ -1,14 +1,140 @@ -import { ServerRequestHandler } from 'vscode-languageserver'; +import { ResponseError, ErrorCodes, RequestHandler } from 'vscode-languageserver'; +import { TopLevelSection } from '../context/ContextType'; +import { getEntityMap } from '../context/SectionContextBuilder'; +import { Parameter } from '../context/semantic/Entity'; +import { parseIdentifiable } from '../protocol/LspParser'; +import { Identifiable } from '../protocol/LspTypes'; import { ServerComponents } from '../server/ServerComponents'; +import { analyzeCapabilities } from '../stacks/actions/CapabilityAnalyzer'; +import { parseStackActionParams, parseTemplateUriParams } from '../stacks/actions/StackActionParser'; +import { + GetCapabilitiesResult, + TemplateUri, + GetParametersResult, + CreateStackActionParams, + CreateStackActionResult, + GetStackActionStatusResult, +} from '../stacks/actions/StackActionRequestType'; import { ListStacksParams, ListStacksResult } from '../stacks/StackRequestType'; import { LoggerFactory } from '../telemetry/LoggerFactory'; import { extractErrorMessage } from '../utils/Errors'; +import { parseWithPrettyError } from '../utils/ZodErrorWrapper'; const log = LoggerFactory.getLogger('StackHandler'); +export function getParametersHandler( + components: ServerComponents, +): RequestHandler { + return (rawParams) => { + log.debug({ Handler: 'getParametersHandler', rawParams }); + + try { + const params = parseWithPrettyError(parseTemplateUriParams, rawParams); + const syntaxTree = components.syntaxTreeManager.getSyntaxTree(params); + if (syntaxTree) { + const parametersMap = getEntityMap(syntaxTree, TopLevelSection.Parameters); + if (parametersMap) { + const parameters = [...parametersMap.values()].map((context) => context.entity as Parameter); + return { + parameters, + }; + } + } + + return { + parameters: [], + }; + } catch (error) { + handleStackActionError(error, 'Failed to get parameters'); + } + }; +} + +export function createValidationHandler( + components: ServerComponents, +): RequestHandler { + return async (rawParams) => { + log.debug({ Handler: 'createValidationHandler', rawParams }); + + try { + const params = parseWithPrettyError(parseStackActionParams, rawParams); + return await components.validationWorkflowService.start(params); + } catch (error) { + handleStackActionError(error, 'Failed to start validation workflow'); + } + }; +} + +export function createDeploymentHandler( + components: ServerComponents, +): RequestHandler { + return async (rawParams) => { + log.debug({ Handler: 'createDeploymentHandler', rawParams }); + + try { + const params = parseWithPrettyError(parseStackActionParams, rawParams); + return await components.deploymentWorkflowService.start(params); + } catch (error) { + handleStackActionError(error, 'Failed to start deployment workflow'); + } + }; +} + +export function getValidationStatusHandler( + components: ServerComponents, +): RequestHandler { + return (rawParams) => { + log.debug({ Handler: 'getValidationStatusHandler', rawParams }); + + try { + const params = parseWithPrettyError(parseIdentifiable, rawParams); + return components.validationWorkflowService.getStatus(params); + } catch (error) { + handleStackActionError(error, 'Failed to get validation status'); + } + }; +} + +export function getDeploymentStatusHandler( + components: ServerComponents, +): RequestHandler { + return (rawParams) => { + log.debug({ Handler: 'getDeploymentStatusHandler', rawParams }); + + try { + const params = parseWithPrettyError(parseIdentifiable, rawParams); + return components.deploymentWorkflowService.getStatus(params); + } catch (error) { + handleStackActionError(error, 'Failed to get deployment status'); + } + }; +} + +export function getCapabilitiesHandler( + components: ServerComponents, +): RequestHandler { + return async (rawParams) => { + log.debug({ Handler: 'getCapabilitiesHandler', rawParams }); + + try { + const params = parseWithPrettyError(parseTemplateUriParams, rawParams); + const document = components.documentManager.get(params); + if (!document) { + throw new ResponseError(ErrorCodes.InvalidRequest, 'Template body document not available'); + } + + const capabilities = await analyzeCapabilities(document, components.cfnService); + + return { capabilities }; + } catch (error) { + handleStackActionError(error, 'Failed to analyze template capabilities'); + } + }; +} + export function listStacksHandler( components: ServerComponents, -): ServerRequestHandler { +): RequestHandler { return async (params: ListStacksParams): Promise => { try { if (params.statusToInclude?.length && params.statusToExclude?.length) { @@ -21,3 +147,13 @@ export function listStacksHandler( } }; } + +function handleStackActionError(error: unknown, contextMessage: string): never { + if (error instanceof ResponseError) { + throw error; + } + if (error instanceof TypeError) { + throw new ResponseError(ErrorCodes.InvalidParams, error.message); + } + throw new ResponseError(ErrorCodes.InternalError, `${contextMessage}: ${extractErrorMessage(error)}`); +} diff --git a/src/handlers/TemplateHandler.ts b/src/handlers/TemplateHandler.ts deleted file mode 100644 index 296c7f3e..00000000 --- a/src/handlers/TemplateHandler.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { ResponseError, ErrorCodes, RequestHandler } from 'vscode-languageserver'; -import { TopLevelSection } from '../context/ContextType'; -import { getEntityMap } from '../context/SectionContextBuilder'; -import { Parameter } from '../context/semantic/Entity'; -import { parseIdentifiable } from '../protocol/LspParser'; -import { Identifiable } from '../protocol/LspTypes'; -import { ServerComponents } from '../server/ServerComponents'; -import { LoggerFactory } from '../telemetry/LoggerFactory'; -import { analyzeCapabilities } from '../templates/CapabilityAnalyzer'; -import { parseTemplateActionParams, parseTemplateMetadataParams } from '../templates/TemplateParser'; -import { - TemplateMetadataParams, - GetParametersResult, - GetCapabilitiesResult, - TemplateActionParams, - TemplateActionResult, - TemplateStatusResult, -} from '../templates/TemplateRequestType'; -import { extractErrorMessage } from '../utils/Errors'; -import { parseWithPrettyError } from '../utils/ZodErrorWrapper'; - -const log = LoggerFactory.getLogger('TemplateHandler'); - -export function templateParametersHandler( - components: ServerComponents, -): RequestHandler { - return (rawParams) => { - log.debug({ Handler: 'TemplateParameters', rawParams }); - - try { - const params = parseWithPrettyError(parseTemplateMetadataParams, rawParams); - const syntaxTree = components.syntaxTreeManager.getSyntaxTree(params.uri); - if (syntaxTree) { - const parametersMap = getEntityMap(syntaxTree, TopLevelSection.Parameters); - if (parametersMap) { - const parameters = [...parametersMap.values()].map((context) => context.entity as Parameter); - return { - parameters, - }; - } - } - - return { - parameters: [], - }; - } catch (error) { - handleTemplateError(error, 'Failed to get parameters'); - } - }; -} - -export function templateValidationCreateHandler( - components: ServerComponents, -): RequestHandler { - return async (rawParams) => { - log.debug({ Handler: 'TemplateValidationCreate', rawParams }); - - try { - const params = parseWithPrettyError(parseTemplateActionParams, rawParams); - return await components.validationWorkflowService.start(params); - } catch (error) { - handleTemplateError(error, 'Failed to start validation workflow'); - } - }; -} - -export function templateDeploymentCreateHandler( - components: ServerComponents, -): RequestHandler { - return async (rawParams) => { - log.debug({ Handler: 'TemplateDeploymentCreate', rawParams }); - - try { - const params = parseWithPrettyError(parseTemplateActionParams, rawParams); - return await components.deploymentWorkflowService.start(params); - } catch (error) { - handleTemplateError(error, 'Failed to start deployment workflow'); - } - }; -} - -export function templateValidationStatusHandler( - components: ServerComponents, -): RequestHandler { - return (rawParams) => { - log.debug({ Handler: 'TemplateValidationStatus', rawParams }); - - try { - const params = parseWithPrettyError(parseIdentifiable, rawParams); - return components.validationWorkflowService.getStatus(params); - } catch (error) { - handleTemplateError(error, 'Failed to get validation status'); - } - }; -} - -export function templateDeploymentStatusHandler( - components: ServerComponents, -): RequestHandler { - return (rawParams) => { - log.debug({ Handler: 'TemplateDeploymentStatus', rawParams }); - - try { - const params = parseWithPrettyError(parseIdentifiable, rawParams); - return components.deploymentWorkflowService.getStatus(params); - } catch (error) { - handleTemplateError(error, 'Failed to get deployment status'); - } - }; -} - -export function templateCapabilitiesHandler( - components: ServerComponents, -): RequestHandler { - return async (rawParams) => { - log.debug({ Handler: 'TemplateCapabilities', rawParams }); - - try { - const params = parseWithPrettyError(parseTemplateMetadataParams, rawParams); - const document = components.documentManager.get(params.uri); - if (!document) { - throw new ResponseError(ErrorCodes.InvalidRequest, 'Template body document not available'); - } - - const capabilities = await analyzeCapabilities(document, components.cfnService); - - return { capabilities }; - } catch (error) { - handleTemplateError(error, 'Failed to analyze template capabilities'); - } - }; -} - -function handleTemplateError(error: unknown, contextMessage: string): never { - if (error instanceof ResponseError) { - throw error; - } - if (error instanceof TypeError) { - throw new ResponseError(ErrorCodes.InvalidParams, error.message); - } - throw new ResponseError(ErrorCodes.InternalError, `${contextMessage}: ${extractErrorMessage(error)}`); -} diff --git a/src/protocol/LspConnection.ts b/src/protocol/LspConnection.ts index ff159698..9cbbcfec 100644 --- a/src/protocol/LspConnection.ts +++ b/src/protocol/LspConnection.ts @@ -15,7 +15,6 @@ import { LspDocuments } from './LspDocuments'; import { LspHandlers } from './LspHandlers'; import { LspResourceHandlers } from './LspResourceHandlers'; import { LspStackHandlers } from './LspStackHandlers'; -import { LspTemplateHandlers } from './LspTemplateHandlers'; import { LspWorkspace } from './LspWorkspace'; type LspConnectionHandlers = { @@ -32,7 +31,6 @@ export type LspFeatures = { communication: LspCommunication; handlers: LspHandlers; authHandlers: LspAuthHandlers; - templateHandlers: LspTemplateHandlers; stackHandlers: LspStackHandlers; resourceHandlers: LspResourceHandlers; }; @@ -44,7 +42,6 @@ export class LspConnection { private readonly communication: LspCommunication; private readonly handlers: LspHandlers; private readonly authHandlers: LspAuthHandlers; - private readonly templateHandlers: LspTemplateHandlers; private readonly stackHandlers: LspStackHandlers; private readonly resourceHandlers: LspResourceHandlers; @@ -67,7 +64,6 @@ export class LspConnection { this.communication = new LspCommunication(this.connection); this.handlers = new LspHandlers(this.connection); this.authHandlers = new LspAuthHandlers(this.connection); - this.templateHandlers = new LspTemplateHandlers(this.connection); this.stackHandlers = new LspStackHandlers(this.connection); this.resourceHandlers = new LspResourceHandlers(this.connection); @@ -104,7 +100,6 @@ export class LspConnection { communication: this.communication, handlers: this.handlers, authHandlers: this.authHandlers, - templateHandlers: this.templateHandlers, stackHandlers: this.stackHandlers, resourceHandlers: this.resourceHandlers, }; diff --git a/src/protocol/LspStackHandlers.ts b/src/protocol/LspStackHandlers.ts index e698c444..3af11a4e 100644 --- a/src/protocol/LspStackHandlers.ts +++ b/src/protocol/LspStackHandlers.ts @@ -1,10 +1,51 @@ -import { Connection, ServerRequestHandler } from 'vscode-languageserver'; -import { ListStacksParams, ListStacksResult, ListStacksRequest } from '../stacks/StackRequestType'; +import { Connection, RequestHandler } from 'vscode-languageserver'; +import { + CreateValidationRequest, + CreateDeploymentRequest, + GetDeploymentStatusRequest, + GetValidationStatusRequest, + GetCapabilitiesRequest, + GetParametersRequest, +} from '../stacks/actions/StackActionProtocol'; +import { + TemplateUri, + CreateStackActionParams, + CreateStackActionResult, + GetStackActionStatusResult, + GetParametersResult, + GetCapabilitiesResult, +} from '../stacks/actions/StackActionRequestType'; +import { ListStacksParams, ListStacksRequest, ListStacksResult } from '../stacks/StackRequestType'; +import { Identifiable } from './LspTypes'; export class LspStackHandlers { constructor(private readonly connection: Connection) {} - onListStacks(handler: ServerRequestHandler) { + onCreateValidation(handler: RequestHandler) { + this.connection.onRequest(CreateValidationRequest.method, handler); + } + + onCreateDeployment(handler: RequestHandler) { + this.connection.onRequest(CreateDeploymentRequest.method, handler); + } + + onGetValidationStatus(handler: RequestHandler) { + this.connection.onRequest(GetValidationStatusRequest.method, handler); + } + + onGetDeploymentStatus(handler: RequestHandler) { + this.connection.onRequest(GetDeploymentStatusRequest.method, handler); + } + + onGetParameters(handler: RequestHandler) { + this.connection.onRequest(GetParametersRequest.method, handler); + } + + onGetCapabilities(handler: RequestHandler) { + this.connection.onRequest(GetCapabilitiesRequest.method, handler); + } + + onListStacks(handler: RequestHandler) { this.connection.onRequest(ListStacksRequest.method, handler); } } diff --git a/src/protocol/LspTemplateHandlers.ts b/src/protocol/LspTemplateHandlers.ts deleted file mode 100644 index 003a0e4f..00000000 --- a/src/protocol/LspTemplateHandlers.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Connection, RequestHandler } from 'vscode-languageserver'; -import { - TemplateActionParams, - TemplateActionResult, - TemplateValidationCreateRequest, - TemplateDeploymentCreateRequest, - TemplateDeploymentStatusRequest, - TemplateStatusResult, - TemplateValidationStatusRequest, - TemplateMetadataParams, - GetParametersRequest, - GetParametersResult, - GetCapabilitiesRequest, - GetCapabilitiesResult, -} from '../templates/TemplateRequestType'; -import { Identifiable } from './LspTypes'; - -export class LspTemplateHandlers { - constructor(private readonly connection: Connection) {} - - onTemplateValidationCreate(handler: RequestHandler) { - this.connection.onRequest(TemplateValidationCreateRequest.method, handler); - } - - onTemplateDeploymentCreate(handler: RequestHandler) { - this.connection.onRequest(TemplateDeploymentCreateRequest.method, handler); - } - - onTemplateValidationStatus(handler: RequestHandler) { - this.connection.onRequest(TemplateValidationStatusRequest.method, handler); - } - - onTemplateDeploymentStatus(handler: RequestHandler) { - this.connection.onRequest(TemplateDeploymentStatusRequest.method, handler); - } - - onGetParameters(handler: RequestHandler) { - this.connection.onRequest(GetParametersRequest.method, handler); - } - - onGetCapabilities(handler: RequestHandler) { - this.connection.onRequest(GetCapabilitiesRequest.method, handler); - } -} diff --git a/src/server/CfnServer.ts b/src/server/CfnServer.ts index f348ff4f..b0ecae58 100644 --- a/src/server/CfnServer.ts +++ b/src/server/CfnServer.ts @@ -24,15 +24,15 @@ import { refreshResourceListHandler, getStackMgmtInfo, } from '../handlers/ResourceHandler'; -import { listStacksHandler } from '../handlers/StackHandler'; import { - templateValidationCreateHandler, - templateDeploymentCreateHandler, - templateValidationStatusHandler, - templateDeploymentStatusHandler, - templateParametersHandler, - templateCapabilitiesHandler, -} from '../handlers/TemplateHandler'; + listStacksHandler, + createValidationHandler, + createDeploymentHandler, + getValidationStatusHandler, + getDeploymentStatusHandler, + getParametersHandler, + getCapabilitiesHandler, +} from '../handlers/StackHandler'; import { LspFeatures } from '../protocol/LspConnection'; import { ServerComponents } from './ServerComponents'; @@ -70,13 +70,12 @@ export class CfnServer { this.features.authHandlers.onBearerCredentialsDelete(bearerCredentialsDeleteHandler(this.components)); this.features.authHandlers.onSsoTokenChanged(ssoTokenChangedHandler(this.components)); - this.features.templateHandlers.onGetParameters(templateParametersHandler(this.components)); - this.features.templateHandlers.onGetCapabilities(templateCapabilitiesHandler(this.components)); - this.features.templateHandlers.onTemplateValidationCreate(templateValidationCreateHandler(this.components)); - this.features.templateHandlers.onTemplateDeploymentCreate(templateDeploymentCreateHandler(this.components)); - this.features.templateHandlers.onTemplateValidationStatus(templateValidationStatusHandler(this.components)); - this.features.templateHandlers.onTemplateDeploymentStatus(templateDeploymentStatusHandler(this.components)); - + this.features.stackHandlers.onGetParameters(getParametersHandler(this.components)); + this.features.stackHandlers.onCreateValidation(createValidationHandler(this.components)); + this.features.stackHandlers.onGetCapabilities(getCapabilitiesHandler(this.components)); + this.features.stackHandlers.onCreateDeployment(createDeploymentHandler(this.components)); + this.features.stackHandlers.onGetValidationStatus(getValidationStatusHandler(this.components)); + this.features.stackHandlers.onGetDeploymentStatus(getDeploymentStatusHandler(this.components)); this.features.stackHandlers.onListStacks(listStacksHandler(this.components)); this.features.resourceHandlers.onListResources(listResourcesHandler(this.components)); diff --git a/src/server/ServerComponents.ts b/src/server/ServerComponents.ts index 4ff89658..e952ddf0 100644 --- a/src/server/ServerComponents.ts +++ b/src/server/ServerComponents.ts @@ -34,12 +34,12 @@ import { DiagnosticCoordinator } from '../services/DiagnosticCoordinator'; import { GuardService } from '../services/guard/GuardService'; import { IacGeneratorService } from '../services/IacGeneratorService'; import { SettingsManager } from '../settings/SettingsManager'; +import { DeploymentWorkflow } from '../stacks/actions/DeploymentWorkflow'; +import { ValidationManager } from '../stacks/actions/ValidationManager'; +import { ValidationWorkflow } from '../stacks/actions/ValidationWorkflow'; import { ClientMessage } from '../telemetry/ClientMessage'; import { StdOutLogger, LoggerFactory } from '../telemetry/LoggerFactory'; import { TelemetryService } from '../telemetry/TelemetryService'; -import { DeploymentWorkflow } from '../templates/DeploymentWorkflow'; -import { ValidationManager } from '../templates/ValidationManager'; -import { ValidationWorkflow } from '../templates/ValidationWorkflow'; export interface Configurable { configure(settingsManager: SettingsManager): void | Promise; @@ -110,7 +110,7 @@ export class ServerComponents { private closeableComponents: Closeable[] = []; constructor( - features: Omit, + features: Omit, overrides: Partial = {}, ) { this.diagnostics = features.diagnostics; diff --git a/src/services/CodeActionService.ts b/src/services/CodeActionService.ts index e24b5705..4c2290a8 100644 --- a/src/services/CodeActionService.ts +++ b/src/services/CodeActionService.ts @@ -14,9 +14,9 @@ import { NodeType } from '../context/syntaxtree/utils/NodeType'; import { DocumentManager } from '../document/DocumentManager'; import { ANALYZE_DIAGNOSTIC } from '../handlers/ExecutionHandler'; import { ServerComponents } from '../server/ServerComponents'; +import { CFN_VALIDATION_SOURCE } from '../stacks/actions/ValidationWorkflow'; import { ClientMessage } from '../telemetry/ClientMessage'; import { LoggerFactory } from '../telemetry/LoggerFactory'; -import { CFN_VALIDATION_SOURCE } from '../templates/ValidationWorkflow'; import { extractErrorMessage } from '../utils/Errors'; import { pointToPosition } from '../utils/TypeConverters'; import { DiagnosticCoordinator } from './DiagnosticCoordinator'; diff --git a/src/services/DiagnosticCoordinator.ts b/src/services/DiagnosticCoordinator.ts index 3d26e44c..6170aebe 100644 --- a/src/services/DiagnosticCoordinator.ts +++ b/src/services/DiagnosticCoordinator.ts @@ -1,7 +1,7 @@ import { Diagnostic, PublishDiagnosticsParams } from 'vscode-languageserver'; import { LspDiagnostics } from '../protocol/LspDiagnostics'; +import { CFN_VALIDATION_SOURCE } from '../stacks/actions/ValidationWorkflow'; import { LoggerFactory } from '../telemetry/LoggerFactory'; -import { CFN_VALIDATION_SOURCE } from '../templates/ValidationWorkflow'; import { extractErrorMessage } from '../utils/Errors'; type SourceToDiagnostics = Map; diff --git a/src/templates/CapabilityAnalyzer.ts b/src/stacks/actions/CapabilityAnalyzer.ts similarity index 86% rename from src/templates/CapabilityAnalyzer.ts rename to src/stacks/actions/CapabilityAnalyzer.ts index 6f42cfba..bcc0ecb7 100644 --- a/src/templates/CapabilityAnalyzer.ts +++ b/src/stacks/actions/CapabilityAnalyzer.ts @@ -1,7 +1,7 @@ import { Capability } from '@aws-sdk/client-cloudformation'; -import { Document } from '../document/Document'; -import { CfnService } from '../services/CfnService'; -import { LoggerFactory } from '../telemetry/LoggerFactory'; +import { Document } from '../../document/Document'; +import { CfnService } from '../../services/CfnService'; +import { LoggerFactory } from '../../telemetry/LoggerFactory'; const log = LoggerFactory.getLogger('CapabilityAnalyzer'); diff --git a/src/templates/DeploymentWorkflow.ts b/src/stacks/actions/DeploymentWorkflow.ts similarity index 66% rename from src/templates/DeploymentWorkflow.ts rename to src/stacks/actions/DeploymentWorkflow.ts index 0423557f..da257f73 100644 --- a/src/templates/DeploymentWorkflow.ts +++ b/src/stacks/actions/DeploymentWorkflow.ts @@ -1,21 +1,21 @@ import { ChangeSetType } from '@aws-sdk/client-cloudformation'; -import { DocumentManager } from '../document/DocumentManager'; -import { Identifiable } from '../protocol/LspTypes'; -import { ServerComponents } from '../server/ServerComponents'; -import { CfnService } from '../services/CfnService'; -import { LoggerFactory } from '../telemetry/LoggerFactory'; +import { DocumentManager } from '../../document/DocumentManager'; +import { Identifiable } from '../../protocol/LspTypes'; +import { ServerComponents } from '../../server/ServerComponents'; +import { CfnService } from '../../services/CfnService'; +import { LoggerFactory } from '../../telemetry/LoggerFactory'; +import { processChangeSet, waitForValidation, waitForDeployment } from './StackActionOperations'; import { - TemplateActionParams, - TemplateActionResult, - TemplateStatus, - WorkflowResult, - TemplateStatusResult, -} from './TemplateRequestType'; -import { processChangeSet, waitForValidation, waitForDeployment } from './TemplateWorkflowOperations'; -import { TemplateWorkflowState, TemplateWorkflow } from './TemplateWorkflowType'; - -export class DeploymentWorkflow implements TemplateWorkflow { - private readonly workflows = new Map(); + CreateStackActionParams, + CreateStackActionResult, + StackActionPhase, + StackActionState, + GetStackActionStatusResult, +} from './StackActionRequestType'; +import { StackActionWorkflowState, StackActionWorkflow } from './StackActionWorkflowType'; + +export class DeploymentWorkflow implements StackActionWorkflow { + private readonly workflows = new Map(); private readonly log = LoggerFactory.getLogger(DeploymentWorkflow); constructor( @@ -27,7 +27,7 @@ export class DeploymentWorkflow implements TemplateWorkflow { return new DeploymentWorkflow(components.cfnService, components.documentManager); } - async start(params: TemplateActionParams): Promise { + async start(params: CreateStackActionParams): Promise { // Check if stack exists to determine CREATE vs UPDATE let changeSetType: ChangeSetType = ChangeSetType.CREATE; try { @@ -44,9 +44,9 @@ export class DeploymentWorkflow implements TemplateWorkflow { id: params.id, changeSetName: changeSetName, stackName: params.stackName, - status: TemplateStatus.VALIDATION_IN_PROGRESS, + phase: StackActionPhase.VALIDATION_IN_PROGRESS, startTime: Date.now(), - result: WorkflowResult.IN_PROGRESS, + state: StackActionState.IN_PROGRESS, }); void this.runDeploymentAsync(params.id, changeSetName, params.stackName, changeSetType); @@ -58,15 +58,15 @@ export class DeploymentWorkflow implements TemplateWorkflow { }; } - getStatus(params: Identifiable): TemplateStatusResult { + getStatus(params: Identifiable): GetStackActionStatusResult { const workflow = this.workflows.get(params.id); if (!workflow) { throw new Error(`Workflow not found: ${params.id}`); } return { - status: workflow.status, - result: workflow.result, + phase: workflow.phase, + state: workflow.state, changes: workflow.changes, id: workflow.id, }; @@ -90,19 +90,19 @@ export class DeploymentWorkflow implements TemplateWorkflow { this.workflows.set(workflowId, { ...existingWorkflow, - status: validationResult.status, - result: validationResult.result, + phase: validationResult.phase, + state: validationResult.state, changes: validationResult.changes, }); - if (validationResult.result === WorkflowResult.FAILED) { + if (validationResult.state === StackActionState.FAILED) { return; } this.workflows.set(workflowId, { ...existingWorkflow, - status: TemplateStatus.VALIDATION_COMPLETE, - result: WorkflowResult.IN_PROGRESS, + phase: StackActionPhase.VALIDATION_COMPLETE, + state: StackActionState.IN_PROGRESS, changes: validationResult.changes, }); @@ -113,8 +113,8 @@ export class DeploymentWorkflow implements TemplateWorkflow { this.workflows.set(workflowId, { ...existingWorkflow, - status: TemplateStatus.DEPLOYMENT_IN_PROGRESS, - result: WorkflowResult.IN_PROGRESS, + phase: StackActionPhase.DEPLOYMENT_IN_PROGRESS, + state: StackActionState.IN_PROGRESS, changes: validationResult.changes, }); @@ -122,16 +122,16 @@ export class DeploymentWorkflow implements TemplateWorkflow { this.workflows.set(workflowId, { ...existingWorkflow, - status: deploymentResult.status, - result: deploymentResult.result, + phase: deploymentResult.phase, + state: deploymentResult.state, changes: validationResult.changes, }); } catch (error) { this.log.error({ error, workflowId }, 'Deployment workflow failed'); this.workflows.set(workflowId, { ...existingWorkflow, - status: validationResult ? TemplateStatus.DEPLOYMENT_FAILED : TemplateStatus.VALIDATION_FAILED, - result: WorkflowResult.FAILED, + phase: validationResult ? StackActionPhase.DEPLOYMENT_FAILED : StackActionPhase.VALIDATION_FAILED, + state: StackActionState.FAILED, changes: validationResult?.changes, }); } diff --git a/src/templates/TemplateWorkflowOperations.ts b/src/stacks/actions/StackActionOperations.ts similarity index 80% rename from src/templates/TemplateWorkflowOperations.ts rename to src/stacks/actions/StackActionOperations.ts index d0646103..047e59e0 100644 --- a/src/templates/TemplateWorkflowOperations.ts +++ b/src/stacks/actions/StackActionOperations.ts @@ -2,25 +2,25 @@ import { Change, ChangeSetType } from '@aws-sdk/client-cloudformation'; import { WaiterState } from '@smithy/util-waiter'; import { v4 as uuidv4 } from 'uuid'; import { ResponseError, ErrorCodes } from 'vscode-languageserver'; -import { DocumentManager } from '../document/DocumentManager'; -import { CfnService } from '../services/CfnService'; -import { LoggerFactory } from '../telemetry/LoggerFactory'; -import { extractErrorMessage } from '../utils/Errors'; -import { retryWithExponentialBackoff } from '../utils/Retry'; -import { TemplateChange, TemplateStatus, WorkflowResult, TemplateActionParams } from './TemplateRequestType'; +import { DocumentManager } from '../../document/DocumentManager'; +import { CfnService } from '../../services/CfnService'; +import { LoggerFactory } from '../../telemetry/LoggerFactory'; +import { extractErrorMessage } from '../../utils/Errors'; +import { retryWithExponentialBackoff } from '../../utils/Retry'; +import { StackChange, StackActionPhase, StackActionState, CreateStackActionParams } from './StackActionRequestType'; import { - TemplateWorkflowState, + StackActionWorkflowState, ValidationWaitForResult, DeploymentWaitForResult, changeSetNamePrefix, -} from './TemplateWorkflowType'; +} from './StackActionWorkflowType'; -const logger = LoggerFactory.getLogger('TemplateWorkflowOperations'); +const logger = LoggerFactory.getLogger('StackActionOperations'); export async function processChangeSet( cfnService: CfnService, documentManager: DocumentManager, - params: TemplateActionParams, + params: CreateStackActionParams, changeSetType: ChangeSetType, ): Promise { const document = documentManager.get(params.uri); @@ -60,9 +60,9 @@ export async function waitForValidation( }); return { - status: TemplateStatus.VALIDATION_COMPLETE, - result: WorkflowResult.SUCCESSFUL, - changes: mapChangesToTemplateChanges(response.Changes), + phase: StackActionPhase.VALIDATION_COMPLETE, + state: StackActionState.SUCCESSFUL, + changes: mapChangesToStackChanges(response.Changes), reason: result.reason ? String(result.reason) : undefined, }; } else { @@ -71,16 +71,16 @@ export async function waitForValidation( 'Validation failed', ); return { - status: TemplateStatus.VALIDATION_FAILED, - result: WorkflowResult.FAILED, + phase: StackActionPhase.VALIDATION_FAILED, + state: StackActionState.FAILED, reason: result.reason ? String(result.reason) : undefined, // TODO: Return reason as part of LSP Response }; } } catch (error) { logger.error({ error: extractErrorMessage(error) }, 'Validation failed with error'); return { - status: TemplateStatus.VALIDATION_FAILED, - result: WorkflowResult.FAILED, + phase: StackActionPhase.VALIDATION_FAILED, + state: StackActionState.FAILED, reason: extractErrorMessage(error), }; } @@ -103,8 +103,8 @@ export async function waitForDeployment( if (result.state === WaiterState.SUCCESS) { return { - status: TemplateStatus.DEPLOYMENT_COMPLETE, - result: WorkflowResult.SUCCESSFUL, + phase: StackActionPhase.DEPLOYMENT_COMPLETE, + state: StackActionState.SUCCESSFUL, reason: result.reason ? String(result.reason) : undefined, }; } else { @@ -113,16 +113,16 @@ export async function waitForDeployment( 'Deployment failed', ); return { - status: TemplateStatus.DEPLOYMENT_FAILED, - result: WorkflowResult.FAILED, + phase: StackActionPhase.DEPLOYMENT_FAILED, + state: StackActionState.FAILED, reason: result.reason ? String(result.reason) : undefined, // TODO: Return reason as part of LSP Response }; } } catch (error) { logger.error({ error: extractErrorMessage(error) }, 'Deployment failed with error'); return { - status: TemplateStatus.DEPLOYMENT_FAILED, - result: WorkflowResult.FAILED, + phase: StackActionPhase.DEPLOYMENT_FAILED, + state: StackActionState.FAILED, reason: extractErrorMessage(error), }; } @@ -130,7 +130,7 @@ export async function waitForDeployment( export async function deleteStackAndChangeSet( cfnService: CfnService, - workflow: TemplateWorkflowState, + workflow: StackActionWorkflowState, workflowId: string, ): Promise { try { @@ -173,7 +173,7 @@ export async function deleteStackAndChangeSet( export async function deleteChangeSet( cfnService: CfnService, - workflow: TemplateWorkflowState, + workflow: StackActionWorkflowState, workflowId: string, ): Promise { try { @@ -198,7 +198,7 @@ export async function deleteChangeSet( } } -export function mapChangesToTemplateChanges(changes?: Change[]): TemplateChange[] | undefined { +export function mapChangesToStackChanges(changes?: Change[]): StackChange[] | undefined { return changes?.map((change: Change) => ({ type: change.Type, resourceChange: change.ResourceChange diff --git a/src/templates/TemplateParser.ts b/src/stacks/actions/StackActionParser.ts similarity index 58% rename from src/templates/TemplateParser.ts rename to src/stacks/actions/StackActionParser.ts index 89d76aa0..4fba3928 100644 --- a/src/templates/TemplateParser.ts +++ b/src/stacks/actions/StackActionParser.ts @@ -1,6 +1,6 @@ import { Capability } from '@aws-sdk/client-cloudformation'; import { z } from 'zod'; -import { TemplateActionParams, TemplateMetadataParams } from './TemplateRequestType'; +import { CreateStackActionParams, TemplateUri } from './StackActionRequestType'; const CapabilitySchema = z.enum([ Capability.CAPABILITY_AUTO_EXPAND, @@ -15,7 +15,7 @@ const ParameterSchema = z.object({ ResolvedValue: z.string().optional(), }); -const TemplateActionParamsSchema = z.object({ +const StackActionParamsSchema = z.object({ id: z.string().min(1), uri: z.string().min(1), stackName: z.string().min(1).max(128), @@ -23,14 +23,12 @@ const TemplateActionParamsSchema = z.object({ capabilities: z.array(CapabilitySchema).optional(), }); -const GetParametersParamsSchema = z.object({ - uri: z.string().min(1), -}); +const TemplateUriSchema = z.string().min(1); -export function parseTemplateActionParams(input: unknown): TemplateActionParams { - return TemplateActionParamsSchema.parse(input); +export function parseStackActionParams(input: unknown): CreateStackActionParams { + return StackActionParamsSchema.parse(input); } -export function parseTemplateMetadataParams(input: unknown): TemplateMetadataParams { - return GetParametersParamsSchema.parse(input); +export function parseTemplateUriParams(input: unknown): TemplateUri { + return TemplateUriSchema.parse(input); } diff --git a/src/stacks/actions/StackActionProtocol.ts b/src/stacks/actions/StackActionProtocol.ts new file mode 100644 index 00000000..61e2f535 --- /dev/null +++ b/src/stacks/actions/StackActionProtocol.ts @@ -0,0 +1,32 @@ +import { RequestType } from 'vscode-languageserver-protocol'; +import { Identifiable } from '../../protocol/LspTypes'; +import { + TemplateUri, + GetParametersResult, + CreateStackActionParams, + CreateStackActionResult, + GetStackActionStatusResult, + GetCapabilitiesResult, +} from './StackActionRequestType'; + +export const CreateValidationRequest = new RequestType( + 'aws/cfn/stack/validation/create', +); + +export const CreateDeploymentRequest = new RequestType( + 'aws/cfn/stack/deployment/create', +); + +export const GetValidationStatusRequest = new RequestType( + 'aws/cfn/stack/validation/status', +); + +export const GetDeploymentStatusRequest = new RequestType( + 'aws/cfn/stack/deployment/status', +); + +export const GetParametersRequest = new RequestType('aws/cfn/stack/parameters'); + +export const GetCapabilitiesRequest = new RequestType( + 'aws/cfn/stack/capabilities', +); diff --git a/src/stacks/actions/StackActionRequestType.ts b/src/stacks/actions/StackActionRequestType.ts new file mode 100644 index 00000000..803c6674 --- /dev/null +++ b/src/stacks/actions/StackActionRequestType.ts @@ -0,0 +1,61 @@ +import { Parameter, Capability, ResourceChangeDetail } from '@aws-sdk/client-cloudformation'; +import { Parameter as EntityParameter } from '../../context/semantic/Entity'; +import { Identifiable } from '../../protocol/LspTypes'; + +export type CreateStackActionParams = Identifiable & { + uri: string; + stackName: string; + parameters?: Parameter[]; + capabilities?: Capability[]; +}; + +export type CreateStackActionResult = Identifiable & { + changeSetName: string; + stackName: string; +}; + +export type TemplateUri = string; + +export type GetParametersResult = { + parameters: EntityParameter[]; +}; + +export type GetCapabilitiesResult = { + capabilities: Capability[]; +}; + +export type StackChange = { + type?: string; + resourceChange?: { + action?: string; + logicalResourceId?: string; + physicalResourceId?: string; + resourceType?: string; + replacement?: string; + scope?: string[]; + details?: ResourceChangeDetail[]; + }; +}; + +export enum StackActionPhase { + VALIDATION_STARTED = 'VALIDATION_STARTED', + DEPLOYMENT_STARTED = 'DEPLOYMENT_STARTED', + VALIDATION_IN_PROGRESS = 'VALIDATION_IN_PROGRESS', + DEPLOYMENT_IN_PROGRESS = 'DEPLOYMENT_IN_PROGRESS', + VALIDATION_COMPLETE = 'VALIDATION_COMPLETE', + VALIDATION_FAILED = 'VALIDATION_FAILED', + DEPLOYMENT_COMPLETE = 'DEPLOYMENT_COMPLETE', + DEPLOYMENT_FAILED = 'DEPLOYMENT_FAILED', +} + +export enum StackActionState { + IN_PROGRESS = 'IN_PROGRESS', + SUCCESSFUL = 'SUCCESSFUL', + FAILED = 'FAILED', +} + +export type GetStackActionStatusResult = Identifiable & { + state: StackActionState; + phase: StackActionPhase; + changes?: StackChange[]; +}; diff --git a/src/stacks/actions/StackActionWorkflowType.ts b/src/stacks/actions/StackActionWorkflowType.ts new file mode 100644 index 00000000..2820e978 --- /dev/null +++ b/src/stacks/actions/StackActionWorkflowType.ts @@ -0,0 +1,42 @@ +import { Identifiable } from '../../protocol/LspTypes'; +import { AwsEnv } from '../../utils/Environment'; +import { ExtensionName } from '../../utils/ExtensionConfig'; +import { + CreateStackActionParams, + CreateStackActionResult, + GetStackActionStatusResult, + StackActionPhase, + StackActionState, + StackChange, +} from './StackActionRequestType'; + +export type StackActionWorkflowState = { + id: string; + changeSetName: string; + stackName: string; + startTime: number; + state: StackActionState; + phase: StackActionPhase; + changes?: StackChange[]; + lastPolled?: number; +}; + +export type ValidationWaitForResult = { + state: StackActionState; + phase: StackActionPhase; + changes?: StackChange[]; + reason?: string; +}; + +export type DeploymentWaitForResult = { + state: StackActionState; + phase: StackActionPhase; + reason?: string; +}; + +export const changeSetNamePrefix = `${ExtensionName}-${AwsEnv}`.replaceAll(' ', '-'); + +export interface StackActionWorkflow { + start(params: CreateStackActionParams): Promise; + getStatus(params: Identifiable): GetStackActionStatusResult; +} diff --git a/src/templates/Validation.ts b/src/stacks/actions/Validation.ts similarity index 74% rename from src/templates/Validation.ts rename to src/stacks/actions/Validation.ts index 3daccbc2..ee33b322 100644 --- a/src/templates/Validation.ts +++ b/src/stacks/actions/Validation.ts @@ -1,5 +1,5 @@ import { Parameter, Capability } from '@aws-sdk/client-cloudformation'; -import { TemplateStatus, TemplateChange } from './TemplateRequestType'; +import { StackActionPhase, StackChange } from './StackActionRequestType'; export class Validation { private readonly uri: string; @@ -7,8 +7,8 @@ export class Validation { private readonly changeSetName: string; private readonly parameters?: Parameter[]; private capabilities?: Capability[]; - private status: TemplateStatus | undefined; - private changes: TemplateChange[] | undefined; + private phase: StackActionPhase | undefined; + private changes: StackChange[] | undefined; constructor( uri: string, @@ -40,19 +40,19 @@ export class Validation { return this.parameters; } - getStatus(): TemplateStatus | undefined { - return this.status; + getPhase(): StackActionPhase | undefined { + return this.phase; } - setStatus(status: TemplateStatus): void { - this.status = status; + setPhase(status: StackActionPhase): void { + this.phase = status; } - getChanges(): TemplateChange[] | undefined { + getChanges(): StackChange[] | undefined { return this.changes; } - setChanges(changes: TemplateChange[]): void { + setChanges(changes: StackChange[]): void { this.changes = changes; } diff --git a/src/templates/ValidationManager.ts b/src/stacks/actions/ValidationManager.ts similarity index 84% rename from src/templates/ValidationManager.ts rename to src/stacks/actions/ValidationManager.ts index 3f2ff7be..c974dfa0 100644 --- a/src/templates/ValidationManager.ts +++ b/src/stacks/actions/ValidationManager.ts @@ -1,4 +1,4 @@ -import { TemplateChange } from './TemplateRequestType'; +import { StackChange } from './StackActionRequestType'; import { Validation } from './Validation'; export class ValidationManager { @@ -12,7 +12,7 @@ export class ValidationManager { return this.validations.get(stackName); } - setChanges(stackName: string, changes: TemplateChange[]): void { + setChanges(stackName: string, changes: StackChange[]): void { const validation = this.validations.get(stackName); if (validation) { validation.setChanges(changes); diff --git a/src/templates/ValidationWorkflow.ts b/src/stacks/actions/ValidationWorkflow.ts similarity index 70% rename from src/templates/ValidationWorkflow.ts rename to src/stacks/actions/ValidationWorkflow.ts index 716363a6..bd3f5ca5 100644 --- a/src/templates/ValidationWorkflow.ts +++ b/src/stacks/actions/ValidationWorkflow.ts @@ -1,32 +1,27 @@ import { ChangeSetType } from '@aws-sdk/client-cloudformation'; -import { SyntaxTreeManager } from '../context/syntaxtree/SyntaxTreeManager'; -import { DocumentManager } from '../document/DocumentManager'; -import { Identifiable } from '../protocol/LspTypes'; -import { ServerComponents } from '../server/ServerComponents'; -import { CfnService } from '../services/CfnService'; -import { DiagnosticCoordinator } from '../services/DiagnosticCoordinator'; -import { LoggerFactory } from '../telemetry/LoggerFactory'; +import { SyntaxTreeManager } from '../../context/syntaxtree/SyntaxTreeManager'; +import { DocumentManager } from '../../document/DocumentManager'; +import { Identifiable } from '../../protocol/LspTypes'; +import { ServerComponents } from '../../server/ServerComponents'; +import { CfnService } from '../../services/CfnService'; +import { DiagnosticCoordinator } from '../../services/DiagnosticCoordinator'; +import { LoggerFactory } from '../../telemetry/LoggerFactory'; +import { deleteStackAndChangeSet, deleteChangeSet, processChangeSet, waitForValidation } from './StackActionOperations'; import { - TemplateActionParams, - TemplateActionResult, - TemplateStatus, - WorkflowResult, - TemplateStatusResult, -} from './TemplateRequestType'; -import { - deleteStackAndChangeSet, - deleteChangeSet, - processChangeSet, - waitForValidation, -} from './TemplateWorkflowOperations'; -import { TemplateWorkflowState, TemplateWorkflow } from './TemplateWorkflowType'; + CreateStackActionParams, + CreateStackActionResult, + StackActionPhase, + StackActionState, + GetStackActionStatusResult, +} from './StackActionRequestType'; +import { StackActionWorkflowState, StackActionWorkflow } from './StackActionWorkflowType'; import { Validation } from './Validation'; import { ValidationManager } from './ValidationManager'; export const CFN_VALIDATION_SOURCE = 'CFN Dry-Run'; -export class ValidationWorkflow implements TemplateWorkflow { - private readonly workflows = new Map(); +export class ValidationWorkflow implements StackActionWorkflow { + private readonly workflows = new Map(); private readonly log = LoggerFactory.getLogger(ValidationWorkflow); constructor( @@ -47,7 +42,7 @@ export class ValidationWorkflow implements TemplateWorkflow { ); } - async start(params: TemplateActionParams): Promise { + async start(params: CreateStackActionParams): Promise { // Check if stack exists to determine CREATE vs UPDATE let changeSetType: ChangeSetType = ChangeSetType.CREATE; try { @@ -67,7 +62,7 @@ export class ValidationWorkflow implements TemplateWorkflow { params.parameters, params.capabilities, ); - validation.setStatus(TemplateStatus.VALIDATION_IN_PROGRESS); + validation.setPhase(StackActionPhase.VALIDATION_IN_PROGRESS); this.validationManager.add(validation); // Set initial workflow state @@ -75,9 +70,9 @@ export class ValidationWorkflow implements TemplateWorkflow { id: params.id, changeSetName: changeSetName, stackName: params.stackName, - status: TemplateStatus.VALIDATION_IN_PROGRESS, + phase: StackActionPhase.VALIDATION_IN_PROGRESS, startTime: Date.now(), - result: WorkflowResult.IN_PROGRESS, + state: StackActionState.IN_PROGRESS, }); void this.runValidationAsync(params.id, changeSetName, params.stackName, changeSetType); @@ -89,15 +84,15 @@ export class ValidationWorkflow implements TemplateWorkflow { }; } - getStatus(params: Identifiable): TemplateStatusResult { + getStatus(params: Identifiable): GetStackActionStatusResult { const workflow = this.workflows.get(params.id); if (!workflow) { throw new Error(`Workflow not found: ${params.id}`); } return { - status: workflow.status, - result: workflow.result, + phase: workflow.phase, + state: workflow.state, changes: workflow.changes, id: workflow.id, }; @@ -120,7 +115,7 @@ export class ValidationWorkflow implements TemplateWorkflow { const validation = this.validationManager.get(stackName); if (validation) { - validation.setStatus(result.status); + validation.setPhase(result.phase); if (result.changes) { validation.setChanges(result.changes); } @@ -128,8 +123,8 @@ export class ValidationWorkflow implements TemplateWorkflow { this.workflows.set(workflowId, { ...existingWorkflow, - status: result.status, - result: result.result, + phase: result.phase, + state: result.state, changes: result.changes, }); } catch (error) { @@ -137,13 +132,13 @@ export class ValidationWorkflow implements TemplateWorkflow { const validation = this.validationManager.get(stackName); if (validation) { - validation.setStatus(TemplateStatus.VALIDATION_FAILED); + validation.setPhase(StackActionPhase.VALIDATION_FAILED); } this.workflows.set(workflowId, { ...existingWorkflow, - status: TemplateStatus.VALIDATION_FAILED, - result: WorkflowResult.FAILED, + phase: StackActionPhase.VALIDATION_FAILED, + state: StackActionState.FAILED, }); } finally { // Cleanup validation object to prevent memory leaks diff --git a/src/templates/TemplateRequestType.ts b/src/templates/TemplateRequestType.ts deleted file mode 100644 index c4988067..00000000 --- a/src/templates/TemplateRequestType.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { Parameter, Capability, ResourceChangeDetail } from '@aws-sdk/client-cloudformation'; -import { RequestType } from 'vscode-languageserver-protocol'; -import { Parameter as EntityParameter } from '../context/semantic/Entity'; -import { Identifiable } from '../protocol/LspTypes'; - -export type TemplateActionParams = Identifiable & { - uri: string; - stackName: string; - parameters?: Parameter[]; - capabilities?: Capability[]; -}; - -export type TemplateActionResult = Identifiable & { - id: string; - changeSetName: string; - stackName: string; -}; - -export type TemplateMetadataParams = { - uri: string; -}; - -export type GetParametersResult = { - parameters: EntityParameter[]; -}; - -export type TemplateChange = { - type?: string; - resourceChange?: { - action?: string; - logicalResourceId?: string; - physicalResourceId?: string; - resourceType?: string; - replacement?: string; - scope?: string[]; - details?: ResourceChangeDetail[]; - }; -}; - -export enum TemplateStatus { - VALIDATION_STARTED = 'VALIDATION_STARTED', - DEPLOYMENT_STARTED = 'DEPLOYMENT_STARTED', - VALIDATION_IN_PROGRESS = 'VALIDATION_IN_PROGRESS', - DEPLOYMENT_IN_PROGRESS = 'DEPLOYMENT_IN_PROGRESS', - VALIDATION_COMPLETE = 'VALIDATION_COMPLETE', - VALIDATION_FAILED = 'VALIDATION_FAILED', - DEPLOYMENT_COMPLETE = 'DEPLOYMENT_COMPLETE', - DEPLOYMENT_FAILED = 'DEPLOYMENT_FAILED', -} - -export enum WorkflowResult { - IN_PROGRESS = 'IN_PROGRESS', - SUCCESSFUL = 'SUCCESSFUL', - FAILED = 'FAILED', -} - -export type TemplateStatusResult = Identifiable & { - status: TemplateStatus; - result: WorkflowResult; - changes?: TemplateChange[]; -}; - -export const TemplateValidationCreateRequest = new RequestType( - 'aws/cfn/template/validation/create', -); - -export const TemplateDeploymentCreateRequest = new RequestType( - 'aws/cfn/template/deployment/create', -); - -export const TemplateValidationStatusRequest = new RequestType( - 'aws/cfn/template/validation/status', -); - -export const TemplateDeploymentStatusRequest = new RequestType( - 'aws/cfn/template/deployment/status', -); - -export const GetParametersRequest = new RequestType( - 'aws/cfn/template/parameters', -); - -export type GetCapabilitiesResult = { - capabilities: Capability[]; -}; - -export const GetCapabilitiesRequest = new RequestType( - 'aws/cfn/template/capabilities', -); diff --git a/src/templates/TemplateWorkflowType.ts b/src/templates/TemplateWorkflowType.ts deleted file mode 100644 index 4838da18..00000000 --- a/src/templates/TemplateWorkflowType.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Identifiable } from '../protocol/LspTypes'; -import { AwsEnv } from '../utils/Environment'; -import { ExtensionName } from '../utils/ExtensionConfig'; -import { - TemplateActionParams, - TemplateActionResult, - TemplateStatusResult, - TemplateStatus, - WorkflowResult, - TemplateChange, -} from './TemplateRequestType'; - -export type TemplateWorkflowState = { - id: string; - changeSetName: string; - stackName: string; - status: TemplateStatus; - startTime: number; - result: WorkflowResult; - changes?: TemplateChange[]; - lastPolled?: number; -}; - -export type ValidationWaitForResult = { - status: TemplateStatus; - result: WorkflowResult; - changes?: TemplateChange[]; - reason?: string; -}; - -export type DeploymentWaitForResult = { - status: TemplateStatus; - result: WorkflowResult; - reason?: string; -}; - -export const changeSetNamePrefix = `${ExtensionName}-${AwsEnv}`.replaceAll(' ', '-'); - -export interface TemplateWorkflow { - start(params: TemplateActionParams): Promise; - getStatus(params: Identifiable): TemplateStatusResult; -} diff --git a/tst/unit/handlers/StackHandler.test.ts b/tst/unit/handlers/StackHandler.test.ts index 72e14eca..ba376cad 100644 --- a/tst/unit/handlers/StackHandler.test.ts +++ b/tst/unit/handlers/StackHandler.test.ts @@ -1,116 +1,325 @@ -import { StackSummary, StackStatus } from '@aws-sdk/client-cloudformation'; -import { describe, it, expect, vi } from 'vitest'; -import { CancellationToken, ResultProgressReporter, WorkDoneProgressReporter } from 'vscode-languageserver'; -import { listStacksHandler } from '../../../src/handlers/StackHandler'; +import { Capability, StackSummary, StackStatus } from '@aws-sdk/client-cloudformation'; +import { StubbedInstance } from 'ts-sinon'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { CancellationToken, ResponseError, ErrorCodes } from 'vscode-languageserver'; +import { Context } from '../../../src/context/Context'; +import * as SectionContextBuilder from '../../../src/context/SectionContextBuilder'; +import { SyntaxTree } from '../../../src/context/syntaxtree/SyntaxTree'; +import { SyntaxTreeManager } from '../../../src/context/syntaxtree/SyntaxTreeManager'; +import { Document } from '../../../src/document/Document'; +import { + getCapabilitiesHandler, + getParametersHandler, + createValidationHandler, + createDeploymentHandler, + getValidationStatusHandler, + getDeploymentStatusHandler, + listStacksHandler, +} from '../../../src/handlers/StackHandler'; +import { analyzeCapabilities } from '../../../src/stacks/actions/CapabilityAnalyzer'; +import { + TemplateUri, + GetCapabilitiesResult, + GetParametersResult, + StackActionPhase, + StackActionState, +} from '../../../src/stacks/actions/StackActionRequestType'; import { ListStacksParams, ListStacksResult } from '../../../src/stacks/StackRequestType'; -import { GetParametersResult } from '../../../src/templates/TemplateRequestType'; +import { + createMockComponents, + createMockSyntaxTreeManager, + MockedServerComponents, +} from '../../utils/MockServerComponents'; -describe('StackHandler', () => { - const mockParams = {} as ListStacksParams; +vi.mock('../../../src/context/SectionContextBuilder', () => ({ + getEntityMap: vi.fn(), +})); + +// Mock the parsers +vi.mock('../../../src/protocol/LspParser', () => ({ + parseIdentifiable: vi.fn((input) => input), +})); + +vi.mock('../../../src/stacks/actions/StackActionParser', () => ({ + parseStackActionParams: vi.fn((input) => input), + parseTemplateUriParams: vi.fn((input) => input), +})); + +vi.mock('../../../src/utils/ZodErrorWrapper', () => ({ + parseWithPrettyError: vi.fn((parser, input) => parser(input)), +})); + +vi.mock('../../../src/stacks/actions/CapabilityAnalyzer', () => ({ + analyzeCapabilities: vi.fn(), +})); + +describe('StackActionHandler', () => { + let mockComponents: MockedServerComponents; + let syntaxTreeManager: StubbedInstance; + let getEntityMapSpy: any; const mockToken = {} as CancellationToken; - const mockWorkDoneProgress = {} as WorkDoneProgressReporter; - const mockResultProgress = {} as ResultProgressReporter; - - it('should return stacks on success', async () => { - const mockStacks: StackSummary[] = [ - { - StackName: 'test-stack', - StackStatus: 'CREATE_COMPLETE', - } as StackSummary, - ]; - - const mockComponents = { - cfnService: { - listStacks: vi.fn().mockResolvedValue(mockStacks), - }, - } as any; - - const handler = listStacksHandler(mockComponents); - const result = (await handler( - mockParams, - mockToken, - mockWorkDoneProgress, - mockResultProgress, - )) as ListStacksResult; - - expect(result.stacks).toEqual(mockStacks); + + beforeEach(() => { + syntaxTreeManager = createMockSyntaxTreeManager(); + mockComponents = createMockComponents({ syntaxTreeManager }); + getEntityMapSpy = vi.mocked(SectionContextBuilder.getEntityMap); + mockComponents.validationWorkflowService.start.reset(); + mockComponents.validationWorkflowService.getStatus.reset(); + mockComponents.deploymentWorkflowService.start.reset(); + mockComponents.deploymentWorkflowService.getStatus.reset(); + vi.clearAllMocks(); + }); + + describe('stackActionParametersHandler', () => { + it('returns empty array when no syntax tree found', () => { + const templateUri: TemplateUri = 'test://template.yaml'; + syntaxTreeManager.getSyntaxTree.withArgs(templateUri).returns(undefined); + + const handler = getParametersHandler(mockComponents); + const result = handler(templateUri, mockToken) as GetParametersResult; + + expect(result).toEqual({ parameters: [] }); + }); + + it('returns empty array when getEntityMap returns undefined', () => { + const templateUri: TemplateUri = 'test://template.yaml'; + const mockSyntaxTree = {} as SyntaxTree; + + syntaxTreeManager.getSyntaxTree.withArgs(templateUri).returns(mockSyntaxTree); + getEntityMapSpy.mockReturnValue(undefined); + + const handler = getParametersHandler(mockComponents); + const result = handler(templateUri, mockToken) as GetParametersResult; + + expect(result).toEqual({ parameters: [] }); + }); + + it('returns parameters when parameters section exists', () => { + const templateUri: TemplateUri = 'test://template.yaml'; + const mockSyntaxTree = {} as SyntaxTree; + const mockParam1 = { name: 'param1', type: 'String' }; + const mockParam2 = { name: 'param2', type: 'Number' }; + const mockContext1 = { entity: mockParam1 } as unknown as Context; + const mockContext2 = { entity: mockParam2 } as unknown as Context; + const parametersMap = new Map([ + ['param1', mockContext1], + ['param2', mockContext2], + ]); + + syntaxTreeManager.getSyntaxTree.withArgs(templateUri).returns(mockSyntaxTree); + getEntityMapSpy.mockReturnValue(parametersMap); + + const handler = getParametersHandler(mockComponents); + const result = handler(templateUri, mockToken) as GetParametersResult; + + expect(result.parameters).toHaveLength(2); + expect(result.parameters[0]).toBe(mockParam1); + expect(result.parameters[1]).toBe(mockParam2); + }); }); - it('should return empty array on error', async () => { - const mockComponents = { - cfnService: { - listStacks: vi.fn().mockRejectedValue(new Error('API Error')), - }, - } as any; - - const handler = listStacksHandler(mockComponents); - const result = (await handler( - mockParams, - mockToken, - mockWorkDoneProgress, - mockResultProgress, - )) as ListStacksResult; - - expect(result.stacks).toEqual([]); + describe('templateCapabilitiesHandler', () => { + it('should return capabilities when document is available', async () => { + const templateUri: TemplateUri = 'test://template.yaml'; + const mockDocument = { getText: vi.fn().mockReturnValue('template content') } as unknown as Document; + const mockCapabilities = ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'] as Capability[]; + + mockComponents.documentManager.get.withArgs(templateUri).returns(mockDocument); + vi.mocked(analyzeCapabilities).mockResolvedValue(mockCapabilities); + + const handler = getCapabilitiesHandler(mockComponents); + const result = (await handler(templateUri, mockToken)) as GetCapabilitiesResult; + + expect(result.capabilities).toEqual(mockCapabilities); + }); + + it('should throw error when document is not available', async () => { + const templateUri: TemplateUri = 'test://template.yaml'; + mockComponents.documentManager.get.withArgs(templateUri).returns(undefined); + + const handler = getCapabilitiesHandler(mockComponents); + + await expect(handler(templateUri, mockToken)).rejects.toThrow(ResponseError); + }); }); - it('should pass statusToInclude to cfnService', async () => { - const mockStacks: StackSummary[] = []; - const mockComponents = { - cfnService: { - listStacks: vi.fn().mockResolvedValue(mockStacks), - }, - } as any; + describe('templateValidationCreateHandler', () => { + it('should delegate to validation service', async () => { + const mockResult = { id: 'test-id', changeSetName: 'cs-123', stackName: 'test-stack' }; + mockComponents.validationWorkflowService.start.resolves(mockResult); + + const handler = createValidationHandler(mockComponents); + const params = { id: 'test-id', uri: 'file:///test.yaml', stackName: 'test-stack' }; + + const result = await handler(params, {} as any); + + expect(mockComponents.validationWorkflowService.start.calledWith(params)).toBe(true); + expect(result).toEqual(mockResult); + }); + + it('should propagate ResponseError from service', async () => { + const responseError = new ResponseError(ErrorCodes.InternalError, 'Service error'); + mockComponents.validationWorkflowService.start.rejects(responseError); + + const handler = createValidationHandler(mockComponents); + const params = { id: 'test-id', uri: 'file:///test.yaml', stackName: 'test-stack' }; + + await expect(handler(params, {} as any)).rejects.toThrow(responseError); + }); - const paramsWithInclude: ListStacksParams = { - statusToInclude: [StackStatus.CREATE_COMPLETE], - }; + it('should wrap other errors as InternalError', async () => { + mockComponents.validationWorkflowService.start.rejects(new Error('Generic error')); - const handler = listStacksHandler(mockComponents); - await handler(paramsWithInclude, mockToken, mockWorkDoneProgress, mockResultProgress); + const handler = createValidationHandler(mockComponents); + const params = { id: 'test-id', uri: 'file:///test.yaml', stackName: 'test-stack' }; - expect(mockComponents.cfnService.listStacks).toHaveBeenCalledWith([StackStatus.CREATE_COMPLETE], undefined); + await expect(handler(params, {} as any)).rejects.toThrow(ResponseError); + }); }); - it('should pass statusToExclude to cfnService', async () => { - const mockStacks: StackSummary[] = []; - const mockComponents = { - cfnService: { - listStacks: vi.fn().mockResolvedValue(mockStacks), - }, - } as any; + describe('stackActionDeploymentCreateHandler', () => { + it('should delegate to deployment service', async () => { + const mockResult = { id: 'test-id', changeSetName: 'cs-123', stackName: 'test-stack' }; + mockComponents.deploymentWorkflowService.start.resolves(mockResult); - const paramsWithExclude: ListStacksParams = { - statusToExclude: [StackStatus.DELETE_COMPLETE], - }; + const handler = createDeploymentHandler(mockComponents); + const params = { id: 'test-id', uri: 'file:///test.yaml', stackName: 'test-stack' }; - const handler = listStacksHandler(mockComponents); - await handler(paramsWithExclude, mockToken, mockWorkDoneProgress, mockResultProgress); + const result = await handler(params, {} as any); - expect(mockComponents.cfnService.listStacks).toHaveBeenCalledWith(undefined, [StackStatus.DELETE_COMPLETE]); + expect(mockComponents.deploymentWorkflowService.start.calledWith(params)).toBe(true); + expect(result).toEqual(mockResult); + }); }); - it('should return empty array when both statusToInclude and statusToExclude are provided', async () => { - const mockComponents = { - cfnService: { - listStacks: vi.fn(), - }, - } as any; - - const paramsWithBoth: ListStacksParams = { - statusToInclude: [StackStatus.CREATE_COMPLETE], - statusToExclude: [StackStatus.DELETE_COMPLETE], - }; - - const handler = listStacksHandler(mockComponents); - const result = (await handler( - paramsWithBoth, - mockToken, - mockWorkDoneProgress, - mockResultProgress, - )) as ListStacksResult; - - expect(result.stacks).toEqual([]); - expect(mockComponents.cfnService.listStacks).not.toHaveBeenCalled(); + describe('stackActionValidationStatusHandler', () => { + it('should delegate to validation service poll', async () => { + const mockResult = { + id: 'test-id', + status: StackActionPhase.VALIDATION_COMPLETE, + result: StackActionState.SUCCESSFUL, + }; + mockComponents.validationWorkflowService.getStatus.resolves(mockResult); + + const handler = getValidationStatusHandler(mockComponents); + const params = { id: 'test-id' }; + + const result = await handler(params, {} as any); + + expect(mockComponents.validationWorkflowService.getStatus.calledWith(params)).toBe(true); + expect(result).toEqual(mockResult); + }); + }); + + describe('stackActionDeploymentStatusHandler', () => { + it('should delegate to deployment service poll', async () => { + const mockResult = { + id: 'test-id', + status: StackActionPhase.DEPLOYMENT_COMPLETE, + result: StackActionState.SUCCESSFUL, + }; + mockComponents.deploymentWorkflowService.getStatus.resolves(mockResult); + + const handler = getDeploymentStatusHandler(mockComponents); + const params = { id: 'test-id' }; + + const result = await handler(params, {} as any); + + expect(mockComponents.deploymentWorkflowService.getStatus.calledWith(params)).toBe(true); + expect(result).toEqual(mockResult); + }); + }); + + describe('StackQueryHandler', () => { + const mockParams = {} as ListStacksParams; + const mockToken = {} as CancellationToken; + + it('should return stacks on success', async () => { + const mockStacks: StackSummary[] = [ + { + StackName: 'test-stack', + StackStatus: 'CREATE_COMPLETE', + } as StackSummary, + ]; + + const mockComponents = { + cfnService: { + listStacks: vi.fn().mockResolvedValue(mockStacks), + }, + } as any; + + const handler = listStacksHandler(mockComponents); + const result = (await handler(mockParams, mockToken)) as ListStacksResult; + + expect(result.stacks).toEqual(mockStacks); + }); + + it('should return empty array on error', async () => { + const mockComponents = { + cfnService: { + listStacks: vi.fn().mockRejectedValue(new Error('API Error')), + }, + } as any; + + const handler = listStacksHandler(mockComponents); + const result = (await handler(mockParams, mockToken)) as ListStacksResult; + + expect(result.stacks).toEqual([]); + }); + + it('should pass statusToInclude to cfnService', async () => { + const mockStacks: StackSummary[] = []; + const mockComponents = { + cfnService: { + listStacks: vi.fn().mockResolvedValue(mockStacks), + }, + } as any; + + const paramsWithInclude: ListStacksParams = { + statusToInclude: [StackStatus.CREATE_COMPLETE], + }; + + const handler = listStacksHandler(mockComponents); + await handler(paramsWithInclude, mockToken); + + expect(mockComponents.cfnService.listStacks).toHaveBeenCalledWith([StackStatus.CREATE_COMPLETE], undefined); + }); + + it('should pass statusToExclude to cfnService', async () => { + const mockStacks: StackSummary[] = []; + const mockComponents = { + cfnService: { + listStacks: vi.fn().mockResolvedValue(mockStacks), + }, + } as any; + + const paramsWithExclude: ListStacksParams = { + statusToExclude: [StackStatus.DELETE_COMPLETE], + }; + + const handler = listStacksHandler(mockComponents); + await handler(paramsWithExclude, mockToken); + + expect(mockComponents.cfnService.listStacks).toHaveBeenCalledWith(undefined, [StackStatus.DELETE_COMPLETE]); + }); + + it('should return empty array when both statusToInclude and statusToExclude are provided', async () => { + const mockComponents = { + cfnService: { + listStacks: vi.fn(), + }, + } as any; + + const paramsWithBoth: ListStacksParams = { + statusToInclude: [StackStatus.CREATE_COMPLETE], + statusToExclude: [StackStatus.DELETE_COMPLETE], + }; + + const handler = listStacksHandler(mockComponents); + const result = (await handler(paramsWithBoth, mockToken)) as ListStacksResult; + + expect(result.stacks).toEqual([]); + expect(mockComponents.cfnService.listStacks).not.toHaveBeenCalled(); + }); }); }); diff --git a/tst/unit/handlers/TemplateHandler.test.ts b/tst/unit/handlers/TemplateHandler.test.ts deleted file mode 100644 index 592bbd27..00000000 --- a/tst/unit/handlers/TemplateHandler.test.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { Capability } from '@aws-sdk/client-cloudformation'; -import { StubbedInstance } from 'ts-sinon'; -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { CancellationToken, ResponseError, ErrorCodes } from 'vscode-languageserver'; -import { Context } from '../../../src/context/Context'; -import * as SectionContextBuilder from '../../../src/context/SectionContextBuilder'; -import { SyntaxTree } from '../../../src/context/syntaxtree/SyntaxTree'; -import { SyntaxTreeManager } from '../../../src/context/syntaxtree/SyntaxTreeManager'; -import { Document } from '../../../src/document/Document'; -import { - templateParametersHandler, - templateCapabilitiesHandler, - templateValidationCreateHandler, - templateDeploymentCreateHandler, - templateValidationStatusHandler, - templateDeploymentStatusHandler, -} from '../../../src/handlers/TemplateHandler'; -import { analyzeCapabilities } from '../../../src/templates/CapabilityAnalyzer'; -import { - TemplateMetadataParams, - GetParametersResult, - GetCapabilitiesResult, - TemplateStatus, - WorkflowResult, -} from '../../../src/templates/TemplateRequestType'; -import { - createMockComponents, - createMockSyntaxTreeManager, - MockedServerComponents, -} from '../../utils/MockServerComponents'; - -vi.mock('../../../src/context/SectionContextBuilder', () => ({ - getEntityMap: vi.fn(), -})); - -// Mock the parsers -vi.mock('../../../src/protocol/LspParser', () => ({ - parseIdentifiable: vi.fn((input) => input), -})); - -vi.mock('../../../src/templates/TemplateParser', () => ({ - parseTemplateActionParams: vi.fn((input) => input), - parseTemplateMetadataParams: vi.fn((input) => input), -})); - -vi.mock('../../../src/utils/ZodErrorWrapper', () => ({ - parseWithPrettyError: vi.fn((parser, input) => parser(input)), -})); - -vi.mock('../../../src/templates/CapabilityAnalyzer', () => ({ - analyzeCapabilities: vi.fn(), -})); - -describe('TemplateHandler', () => { - let mockComponents: MockedServerComponents; - let syntaxTreeManager: StubbedInstance; - let getEntityMapSpy: any; - const mockToken = {} as CancellationToken; - - beforeEach(() => { - syntaxTreeManager = createMockSyntaxTreeManager(); - mockComponents = createMockComponents({ syntaxTreeManager }); - getEntityMapSpy = vi.mocked(SectionContextBuilder.getEntityMap); - mockComponents.validationWorkflowService.start.reset(); - mockComponents.validationWorkflowService.getStatus.reset(); - mockComponents.deploymentWorkflowService.start.reset(); - mockComponents.deploymentWorkflowService.getStatus.reset(); - vi.clearAllMocks(); - }); - - describe('templateParametersHandler', () => { - it('returns empty array when no syntax tree found', () => { - const params: TemplateMetadataParams = { uri: 'test://template.yaml' }; - syntaxTreeManager.getSyntaxTree.withArgs(params.uri).returns(undefined); - - const handler = templateParametersHandler(mockComponents); - const result = handler(params, mockToken) as GetParametersResult; - - expect(result).toEqual({ parameters: [] }); - }); - - it('returns empty array when getEntityMap returns undefined', () => { - const params: TemplateMetadataParams = { uri: 'test://template.yaml' }; - const mockSyntaxTree = {} as SyntaxTree; - - syntaxTreeManager.getSyntaxTree.withArgs(params.uri).returns(mockSyntaxTree); - getEntityMapSpy.mockReturnValue(undefined); - - const handler = templateParametersHandler(mockComponents); - const result = handler(params, mockToken) as GetParametersResult; - - expect(result).toEqual({ parameters: [] }); - }); - - it('returns parameters when parameters section exists', () => { - const params: TemplateMetadataParams = { uri: 'test://template.yaml' }; - const mockSyntaxTree = {} as SyntaxTree; - const mockParam1 = { name: 'param1', type: 'String' }; - const mockParam2 = { name: 'param2', type: 'Number' }; - const mockContext1 = { entity: mockParam1 } as unknown as Context; - const mockContext2 = { entity: mockParam2 } as unknown as Context; - const parametersMap = new Map([ - ['param1', mockContext1], - ['param2', mockContext2], - ]); - - syntaxTreeManager.getSyntaxTree.withArgs(params.uri).returns(mockSyntaxTree); - getEntityMapSpy.mockReturnValue(parametersMap); - - const handler = templateParametersHandler(mockComponents); - const result = handler(params, mockToken) as GetParametersResult; - - expect(result.parameters).toHaveLength(2); - expect(result.parameters[0]).toBe(mockParam1); - expect(result.parameters[1]).toBe(mockParam2); - }); - }); - - describe('templateCapabilitiesHandler', () => { - it('should return capabilities when document is available', async () => { - const params: TemplateMetadataParams = { uri: 'test://template.yaml' }; - const mockDocument = { getText: vi.fn().mockReturnValue('template content') } as unknown as Document; - const mockCapabilities = ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'] as Capability[]; - - mockComponents.documentManager.get.withArgs(params.uri).returns(mockDocument); - vi.mocked(analyzeCapabilities).mockResolvedValue(mockCapabilities); - - const handler = templateCapabilitiesHandler(mockComponents); - const result = (await handler(params, mockToken)) as GetCapabilitiesResult; - - expect(result.capabilities).toEqual(mockCapabilities); - }); - - it('should throw error when document is not available', async () => { - const params: TemplateMetadataParams = { uri: 'test://template.yaml' }; - mockComponents.documentManager.get.withArgs(params.uri).returns(undefined); - - const handler = templateCapabilitiesHandler(mockComponents); - - await expect(handler(params, mockToken)).rejects.toThrow(ResponseError); - }); - }); - - describe('templateValidationCreateHandler', () => { - it('should delegate to validation service', async () => { - const mockResult = { id: 'test-id', changeSetName: 'cs-123', stackName: 'test-stack' }; - mockComponents.validationWorkflowService.start.resolves(mockResult); - - const handler = templateValidationCreateHandler(mockComponents); - const params = { id: 'test-id', uri: 'file:///test.yaml', stackName: 'test-stack' }; - - const result = await handler(params, {} as any); - - expect(mockComponents.validationWorkflowService.start.calledWith(params)).toBe(true); - expect(result).toEqual(mockResult); - }); - - it('should propagate ResponseError from service', async () => { - const responseError = new ResponseError(ErrorCodes.InternalError, 'Service error'); - mockComponents.validationWorkflowService.start.rejects(responseError); - - const handler = templateValidationCreateHandler(mockComponents); - const params = { id: 'test-id', uri: 'file:///test.yaml', stackName: 'test-stack' }; - - await expect(handler(params, {} as any)).rejects.toThrow(responseError); - }); - - it('should wrap other errors as InternalError', async () => { - mockComponents.validationWorkflowService.start.rejects(new Error('Generic error')); - - const handler = templateValidationCreateHandler(mockComponents); - const params = { id: 'test-id', uri: 'file:///test.yaml', stackName: 'test-stack' }; - - await expect(handler(params, {} as any)).rejects.toThrow(ResponseError); - }); - }); - - describe('templateDeploymentCreateHandler', () => { - it('should delegate to deployment service', async () => { - const mockResult = { id: 'test-id', changeSetName: 'cs-123', stackName: 'test-stack' }; - mockComponents.deploymentWorkflowService.start.resolves(mockResult); - - const handler = templateDeploymentCreateHandler(mockComponents); - const params = { id: 'test-id', uri: 'file:///test.yaml', stackName: 'test-stack' }; - - const result = await handler(params, {} as any); - - expect(mockComponents.deploymentWorkflowService.start.calledWith(params)).toBe(true); - expect(result).toEqual(mockResult); - }); - }); - - describe('templateValidationStatusHandler', () => { - it('should delegate to validation service poll', async () => { - const mockResult = { - id: 'test-id', - status: TemplateStatus.VALIDATION_COMPLETE, - result: WorkflowResult.SUCCESSFUL, - }; - mockComponents.validationWorkflowService.getStatus.resolves(mockResult); - - const handler = templateValidationStatusHandler(mockComponents); - const params = { id: 'test-id' }; - - const result = await handler(params, {} as any); - - expect(mockComponents.validationWorkflowService.getStatus.calledWith(params)).toBe(true); - expect(result).toEqual(mockResult); - }); - }); - - describe('templateDeploymentStatusHandler', () => { - it('should delegate to deployment service poll', async () => { - const mockResult = { - id: 'test-id', - status: TemplateStatus.DEPLOYMENT_COMPLETE, - result: WorkflowResult.SUCCESSFUL, - }; - mockComponents.deploymentWorkflowService.getStatus.resolves(mockResult); - - const handler = templateDeploymentStatusHandler(mockComponents); - const params = { id: 'test-id' }; - - const result = await handler(params, {} as any); - - expect(mockComponents.deploymentWorkflowService.getStatus.calledWith(params)).toBe(true); - expect(result).toEqual(mockResult); - }); - }); -}); diff --git a/tst/unit/protocol/LspStackHandlers.test.ts b/tst/unit/protocol/LspStackHandlers.test.ts new file mode 100644 index 00000000..e7846678 --- /dev/null +++ b/tst/unit/protocol/LspStackHandlers.test.ts @@ -0,0 +1,79 @@ +import { StubbedInstance, stubInterface } from 'ts-sinon'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { Connection, RequestHandler } from 'vscode-languageserver/node'; +import { LspStackHandlers } from '../../../src/protocol/LspStackHandlers'; +import { Identifiable } from '../../../src/protocol/LspTypes'; +import { + GetCapabilitiesRequest, + GetParametersRequest, + CreateValidationRequest, + CreateDeploymentRequest, + GetValidationStatusRequest, + GetDeploymentStatusRequest, +} from '../../../src/stacks/actions/StackActionProtocol'; +import { + GetCapabilitiesResult, + TemplateUri, + GetParametersResult, + CreateStackActionParams, + CreateStackActionResult, + GetStackActionStatusResult, +} from '../../../src/stacks/actions/StackActionRequestType'; + +describe('LspTemplateHandlers', () => { + let connection: StubbedInstance; + let stackActionHandlers: LspStackHandlers; + + beforeEach(() => { + connection = stubInterface(); + stackActionHandlers = new LspStackHandlers(connection); + }); + + it('should register onGetParameters handler', () => { + const mockHandler: RequestHandler = vi.fn(); + + stackActionHandlers.onGetParameters(mockHandler); + + expect(connection.onRequest.calledWith(GetParametersRequest.method)).toBe(true); + }); + + it('should register onGetCapabilities handler', () => { + const mockHandler: RequestHandler = vi.fn(); + + stackActionHandlers.onGetCapabilities(mockHandler); + + expect(connection.onRequest.calledWith(GetCapabilitiesRequest.method)).toBe(true); + }); + + it('should register onTemplateValidate handler', () => { + const mockHandler: RequestHandler = vi.fn(); + + stackActionHandlers.onCreateValidation(mockHandler); + + expect(connection.onRequest.calledWith(CreateValidationRequest.method)).toBe(true); + }); + + it('should register onTemplateDeploy handler', () => { + const mockHandler: RequestHandler = vi.fn(); + + stackActionHandlers.onCreateDeployment(mockHandler); + + expect(connection.onRequest.calledWith(CreateDeploymentRequest.method)).toBe(true); + }); + + it('should register onTemplateValidatePoll handler', () => { + const mockHandler: RequestHandler = vi.fn(); + + stackActionHandlers.onGetValidationStatus(mockHandler); + + expect(connection.onRequest.calledWith(GetValidationStatusRequest.method)).toBe(true); + }); + + it('should register onTemplateDeployPoll handler', () => { + const mockHandler: RequestHandler = vi.fn(); + + stackActionHandlers.onGetDeploymentStatus(mockHandler); + + expect(connection.onRequest.calledWith(GetDeploymentStatusRequest.method)).toBe(true); + }); +}); diff --git a/tst/unit/protocol/LspTemplateHandlers.test.ts b/tst/unit/protocol/LspTemplateHandlers.test.ts deleted file mode 100644 index 5ae59030..00000000 --- a/tst/unit/protocol/LspTemplateHandlers.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { StubbedInstance, stubInterface } from 'ts-sinon'; -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { Connection, RequestHandler } from 'vscode-languageserver/node'; -import { LspTemplateHandlers } from '../../../src/protocol/LspTemplateHandlers'; -import { Identifiable } from '../../../src/protocol/LspTypes'; -import { - TemplateMetadataParams, - GetParametersResult, - GetParametersRequest, - GetCapabilitiesResult, - GetCapabilitiesRequest, - TemplateActionParams, - TemplateActionResult, - TemplateValidationCreateRequest, - TemplateDeploymentCreateRequest, - TemplateValidationStatusRequest, - TemplateDeploymentStatusRequest, - TemplateStatusResult, -} from '../../../src/templates/TemplateRequestType'; - -describe('LspTemplateHandlers', () => { - let connection: StubbedInstance; - let templateHandlers: LspTemplateHandlers; - - beforeEach(() => { - connection = stubInterface(); - templateHandlers = new LspTemplateHandlers(connection); - }); - - it('should register onGetParameters handler', () => { - const mockHandler: RequestHandler = vi.fn(); - - templateHandlers.onGetParameters(mockHandler); - - expect(connection.onRequest.calledWith(GetParametersRequest.method)).toBe(true); - }); - - it('should register onGetCapabilities handler', () => { - const mockHandler: RequestHandler = vi.fn(); - - templateHandlers.onGetCapabilities(mockHandler); - - expect(connection.onRequest.calledWith(GetCapabilitiesRequest.method)).toBe(true); - }); - - it('should register onTemplateValidate handler', () => { - const mockHandler: RequestHandler = vi.fn(); - - templateHandlers.onTemplateValidationCreate(mockHandler); - - expect(connection.onRequest.calledWith(TemplateValidationCreateRequest.method)).toBe(true); - }); - - it('should register onTemplateDeploy handler', () => { - const mockHandler: RequestHandler = vi.fn(); - - templateHandlers.onTemplateDeploymentCreate(mockHandler); - - expect(connection.onRequest.calledWith(TemplateDeploymentCreateRequest.method)).toBe(true); - }); - - it('should register onTemplateValidatePoll handler', () => { - const mockHandler: RequestHandler = vi.fn(); - - templateHandlers.onTemplateValidationStatus(mockHandler); - - expect(connection.onRequest.calledWith(TemplateValidationStatusRequest.method)).toBe(true); - }); - - it('should register onTemplateDeployPoll handler', () => { - const mockHandler: RequestHandler = vi.fn(); - - templateHandlers.onTemplateDeploymentStatus(mockHandler); - - expect(connection.onRequest.calledWith(TemplateDeploymentStatusRequest.method)).toBe(true); - }); -}); diff --git a/tst/unit/server/CfnServer.test.ts b/tst/unit/server/CfnServer.test.ts index 56080f60..7c58783b 100644 --- a/tst/unit/server/CfnServer.test.ts +++ b/tst/unit/server/CfnServer.test.ts @@ -8,8 +8,7 @@ import { createMockLspDocuments, createMockLspWorkspace, createMockLspDiagnostics, - createMockLspTemplateHandlers, - createMockStackHandlers, + createMockLspStackHandlers, createMockLspResourceHandlers, } from '../../utils/MockServerComponents'; @@ -32,8 +31,7 @@ describe('CfnServer', () => { communication: createMockLspCommunication(), handlers: createMockLspHandlers(), authHandlers: createMockAuthHandlers(), - templateHandlers: createMockLspTemplateHandlers(), - stackHandlers: createMockStackHandlers(), + stackHandlers: createMockLspStackHandlers(), resourceHandlers: createMockLspResourceHandlers(), }; @@ -71,12 +69,11 @@ describe('CfnServer', () => { expect(mockFeatures.authHandlers.onBearerCredentialsDelete.calledOnce).toBe(true); expect(mockFeatures.authHandlers.onSsoTokenChanged.calledOnce).toBe(true); - expect(mockFeatures.templateHandlers.onGetParameters.calledOnce).toBe(true); - expect(mockFeatures.templateHandlers.onTemplateValidationCreate.calledOnce).toBe(true); - expect(mockFeatures.templateHandlers.onTemplateDeploymentCreate.calledOnce).toBe(true); - expect(mockFeatures.templateHandlers.onTemplateValidationStatus.calledOnce).toBe(true); - expect(mockFeatures.templateHandlers.onTemplateDeploymentStatus.calledOnce).toBe(true); - + expect(mockFeatures.stackHandlers.onGetParameters.calledOnce).toBe(true); + expect(mockFeatures.stackHandlers.onCreateValidation.calledOnce).toBe(true); + expect(mockFeatures.stackHandlers.onCreateDeployment.calledOnce).toBe(true); + expect(mockFeatures.stackHandlers.onGetValidationStatus.calledOnce).toBe(true); + expect(mockFeatures.stackHandlers.onGetDeploymentStatus.calledOnce).toBe(true); expect(mockFeatures.stackHandlers.onListStacks.calledOnce).toBe(true); }); }); diff --git a/tst/unit/services/CodeActionService.test.ts b/tst/unit/services/CodeActionService.test.ts index d41f5a23..89b34bca 100644 --- a/tst/unit/services/CodeActionService.test.ts +++ b/tst/unit/services/CodeActionService.test.ts @@ -8,8 +8,8 @@ import { SyntaxTreeManager } from '../../../src/context/syntaxtree/SyntaxTreeMan import { DocumentManager } from '../../../src/document/DocumentManager'; import { CodeActionService } from '../../../src/services/CodeActionService'; import { DiagnosticCoordinator } from '../../../src/services/DiagnosticCoordinator'; +import { CFN_VALIDATION_SOURCE } from '../../../src/stacks/actions/ValidationWorkflow'; import { ClientMessage } from '../../../src/telemetry/ClientMessage'; -import { CFN_VALIDATION_SOURCE } from '../../../src/templates/ValidationWorkflow'; import { createMockClientMessage } from '../../utils/MockServerComponents'; /* eslint-disable vitest/expect-expect */ diff --git a/tst/unit/services/DiagnosticCoordinator.test.ts b/tst/unit/services/DiagnosticCoordinator.test.ts index f48a9189..1926fa26 100644 --- a/tst/unit/services/DiagnosticCoordinator.test.ts +++ b/tst/unit/services/DiagnosticCoordinator.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { Diagnostic, DiagnosticSeverity, Range, Position } from 'vscode-languageserver'; import { LspDiagnostics } from '../../../src/protocol/LspDiagnostics'; import { DiagnosticCoordinator } from '../../../src/services/DiagnosticCoordinator'; -import { CFN_VALIDATION_SOURCE } from '../../../src/templates/ValidationWorkflow'; +import { CFN_VALIDATION_SOURCE } from '../../../src/stacks/actions/ValidationWorkflow'; describe('DiagnosticCoordinator', () => { let coordinator: DiagnosticCoordinator; diff --git a/tst/unit/templates/CapabilityAnalyzer.test.ts b/tst/unit/stackActions/CapabilityAnalyzer.test.ts similarity index 97% rename from tst/unit/templates/CapabilityAnalyzer.test.ts rename to tst/unit/stackActions/CapabilityAnalyzer.test.ts index 0b468c47..3ced8201 100644 --- a/tst/unit/templates/CapabilityAnalyzer.test.ts +++ b/tst/unit/stackActions/CapabilityAnalyzer.test.ts @@ -2,7 +2,7 @@ import { Capability } from '@aws-sdk/client-cloudformation'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { Document } from '../../../src/document/Document'; import { CfnService } from '../../../src/services/CfnService'; -import { analyzeCapabilities } from '../../../src/templates/CapabilityAnalyzer'; +import { analyzeCapabilities } from '../../../src/stacks/actions/CapabilityAnalyzer'; describe('analyzeCapabilities', () => { const mockDocument = { diff --git a/tst/unit/templates/DeploymentWorkflow.test.ts b/tst/unit/stackActions/DeploymentWorkflow.test.ts similarity index 81% rename from tst/unit/templates/DeploymentWorkflow.test.ts rename to tst/unit/stackActions/DeploymentWorkflow.test.ts index 2986a605..1aff5178 100644 --- a/tst/unit/templates/DeploymentWorkflow.test.ts +++ b/tst/unit/stackActions/DeploymentWorkflow.test.ts @@ -1,11 +1,15 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { DocumentManager } from '../../../src/document/DocumentManager'; import { CfnService } from '../../../src/services/CfnService'; -import { DeploymentWorkflow } from '../../../src/templates/DeploymentWorkflow'; -import { TemplateActionParams, TemplateStatus, WorkflowResult } from '../../../src/templates/TemplateRequestType'; -import { processChangeSet } from '../../../src/templates/TemplateWorkflowOperations'; +import { DeploymentWorkflow } from '../../../src/stacks/actions/DeploymentWorkflow'; +import { processChangeSet } from '../../../src/stacks/actions/StackActionOperations'; +import { + CreateStackActionParams, + StackActionPhase, + StackActionState, +} from '../../../src/stacks/actions/StackActionRequestType'; -vi.mock('../../../src/templates/TemplateWorkflowOperations'); +vi.mock('../../../src/stacks/actions/StackActionOperations'); describe('DeploymentWorkflow', () => { let deploymentWorkflow: DeploymentWorkflow; @@ -21,7 +25,7 @@ describe('DeploymentWorkflow', () => { describe('start', () => { it('should start deployment workflow with CREATE when stack does not exist', async () => { - const params: TemplateActionParams = { + const params: CreateStackActionParams = { id: 'test-id', uri: 'file:///test.yaml', stackName: 'test-stack', @@ -42,7 +46,7 @@ describe('DeploymentWorkflow', () => { }); it('should start deployment workflow with UPDATE when stack exists', async () => { - const params: TemplateActionParams = { + const params: CreateStackActionParams = { id: 'test-id', uri: 'file:///test.yaml', stackName: 'test-stack', @@ -71,9 +75,9 @@ describe('DeploymentWorkflow', () => { id: 'test-id', changeSetName: 'changeset-123', stackName: 'test-stack', - status: TemplateStatus.VALIDATION_IN_PROGRESS, + phase: StackActionPhase.VALIDATION_IN_PROGRESS, startTime: Date.now(), - result: WorkflowResult.IN_PROGRESS, + state: StackActionState.IN_PROGRESS, }; // Directly set workflow state @@ -82,8 +86,8 @@ describe('DeploymentWorkflow', () => { const result = deploymentWorkflow.getStatus(params); expect(result).toEqual({ - status: TemplateStatus.VALIDATION_IN_PROGRESS, - result: WorkflowResult.IN_PROGRESS, + phase: StackActionPhase.VALIDATION_IN_PROGRESS, + state: StackActionState.IN_PROGRESS, changes: undefined, id: 'test-id', }); diff --git a/tst/unit/templates/TemplateParser.test.ts b/tst/unit/stackActions/StackActionParser.test.ts similarity index 73% rename from tst/unit/templates/TemplateParser.test.ts rename to tst/unit/stackActions/StackActionParser.test.ts index b5cc0ce3..3cb125b8 100644 --- a/tst/unit/templates/TemplateParser.test.ts +++ b/tst/unit/stackActions/StackActionParser.test.ts @@ -1,11 +1,11 @@ import { Capability } from '@aws-sdk/client-cloudformation'; import { describe, it, expect } from 'vitest'; import { ZodError } from 'zod'; -import { parseTemplateActionParams, parseTemplateMetadataParams } from '../../../src/templates/TemplateParser'; +import { parseStackActionParams, parseTemplateUriParams } from '../../../src/stacks/actions/StackActionParser'; -describe('TemplateParser', () => { - describe('parseTemplateActionParams', () => { - it('should parse valid template action params', () => { +describe('StackActionParser', () => { + describe('parseStackActionParams', () => { + it('should parse valid stack action params', () => { const input = { id: 'test-id', uri: 'file:///test.yaml', @@ -19,7 +19,7 @@ describe('TemplateParser', () => { capabilities: [Capability.CAPABILITY_IAM], }; - const result = parseTemplateActionParams(input); + const result = parseStackActionParams(input); expect(result).toEqual({ id: 'test-id', @@ -42,7 +42,7 @@ describe('TemplateParser', () => { stackName: 'test-stack', }; - const result = parseTemplateActionParams(input); + const result = parseStackActionParams(input); expect(result).toEqual({ id: 'test-id', @@ -58,7 +58,7 @@ describe('TemplateParser', () => { stackName: 'test-stack', }; - expect(() => parseTemplateActionParams(input)).toThrow(ZodError); + expect(() => parseStackActionParams(input)).toThrow(ZodError); }); it('should throw ZodError for missing id', () => { @@ -67,7 +67,7 @@ describe('TemplateParser', () => { stackName: 'test-stack', }; - expect(() => parseTemplateActionParams(input)).toThrow(ZodError); + expect(() => parseStackActionParams(input)).toThrow(ZodError); }); it('should throw ZodError for empty uri', () => { @@ -77,7 +77,7 @@ describe('TemplateParser', () => { stackName: 'test-stack', }; - expect(() => parseTemplateActionParams(input)).toThrow(ZodError); + expect(() => parseStackActionParams(input)).toThrow(ZodError); }); it('should throw ZodError for empty stackName', () => { @@ -87,7 +87,7 @@ describe('TemplateParser', () => { stackName: '', }; - expect(() => parseTemplateActionParams(input)).toThrow(ZodError); + expect(() => parseStackActionParams(input)).toThrow(ZodError); }); it('should throw ZodError for stackName exceeding 128 characters', () => { @@ -97,7 +97,7 @@ describe('TemplateParser', () => { stackName: 'a'.repeat(129), }; - expect(() => parseTemplateActionParams(input)).toThrow(ZodError); + expect(() => parseStackActionParams(input)).toThrow(ZodError); }); it('should throw ZodError for invalid capability', () => { @@ -108,7 +108,7 @@ describe('TemplateParser', () => { capabilities: ['INVALID_CAPABILITY'], }; - expect(() => parseTemplateActionParams(input)).toThrow(ZodError); + expect(() => parseStackActionParams(input)).toThrow(ZodError); }); it('should parse all valid capabilities', () => { @@ -123,7 +123,7 @@ describe('TemplateParser', () => { ], }; - const result = parseTemplateActionParams(input); + const result = parseStackActionParams(input); expect(result.capabilities).toEqual([ Capability.CAPABILITY_IAM, @@ -147,7 +147,7 @@ describe('TemplateParser', () => { ], }; - const result = parseTemplateActionParams(input); + const result = parseStackActionParams(input); expect(result.parameters?.[0]).toEqual({ ParameterKey: 'Environment', @@ -160,37 +160,25 @@ describe('TemplateParser', () => { describe('parseGetParametersParams', () => { it('should parse valid get parameters params', () => { - const input = { - uri: 'file:///test.yaml', - }; + const input = 'file:///test.yaml'; - const result = parseTemplateMetadataParams(input); + const result = parseTemplateUriParams(input); - expect(result).toEqual({ - uri: 'file:///test.yaml', - }); + expect(result).toEqual('file:///test.yaml'); }); it('should throw ZodError for empty uri', () => { - const input = { - uri: '', - }; - - expect(() => parseTemplateMetadataParams(input)).toThrow(ZodError); - }); - - it('should throw ZodError for missing uri', () => { - const input = {}; + const input = ''; - expect(() => parseTemplateMetadataParams(input)).toThrow(ZodError); + expect(() => parseTemplateUriParams(input)).toThrow(ZodError); }); it('should throw ZodError for null input', () => { - expect(() => parseTemplateMetadataParams(null)).toThrow(ZodError); + expect(() => parseTemplateUriParams(null)).toThrow(ZodError); }); it('should throw ZodError for undefined input', () => { - expect(() => parseTemplateMetadataParams(undefined)).toThrow(ZodError); + expect(() => parseTemplateUriParams(undefined)).toThrow(ZodError); }); }); }); diff --git a/tst/unit/templates/TemplateWorkflowOperations.test.ts b/tst/unit/stackActions/StackActionWorkflowOperations.test.ts similarity index 83% rename from tst/unit/templates/TemplateWorkflowOperations.test.ts rename to tst/unit/stackActions/StackActionWorkflowOperations.test.ts index bb16f0fe..7440d34c 100644 --- a/tst/unit/templates/TemplateWorkflowOperations.test.ts +++ b/tst/unit/stackActions/StackActionWorkflowOperations.test.ts @@ -4,23 +4,27 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { ResponseError } from 'vscode-languageserver'; import { DocumentManager } from '../../../src/document/DocumentManager'; import { CfnService } from '../../../src/services/CfnService'; -import { TemplateActionParams, TemplateStatus, WorkflowResult } from '../../../src/templates/TemplateRequestType'; import { processChangeSet, waitForValidation, waitForDeployment, deleteStackAndChangeSet, deleteChangeSet, - mapChangesToTemplateChanges, -} from '../../../src/templates/TemplateWorkflowOperations'; -import { TemplateWorkflowState } from '../../../src/templates/TemplateWorkflowType'; + mapChangesToStackChanges, +} from '../../../src/stacks/actions/StackActionOperations'; +import { + CreateStackActionParams, + StackActionPhase, + StackActionState, +} from '../../../src/stacks/actions/StackActionRequestType'; +import { StackActionWorkflowState } from '../../../src/stacks/actions/StackActionWorkflowType'; import { ExtensionName } from '../../../src/utils/ExtensionConfig'; vi.mock('../../../src/utils/Retry', () => ({ retryWithExponentialBackoff: vi.fn(), })); -describe('TemplateWorkflowOperations', () => { +describe('StackActionWorkflowOperations', () => { let mockCfnService: CfnService; let mockDocumentManager: DocumentManager; @@ -44,7 +48,7 @@ describe('TemplateWorkflowOperations', () => { describe('processChangeSet', () => { it('should create change set successfully', async () => { - const params: TemplateActionParams = { + const params: CreateStackActionParams = { id: 'test-id', uri: 'file:///test.yaml', stackName: 'test-stack', @@ -72,7 +76,7 @@ describe('TemplateWorkflowOperations', () => { }); it('should throw error when document not found', async () => { - const params: TemplateActionParams = { + const params: CreateStackActionParams = { id: 'test-id', uri: 'file:///missing.yaml', stackName: 'test-stack', @@ -95,8 +99,8 @@ describe('TemplateWorkflowOperations', () => { const result = await waitForDeployment(mockCfnService, 'test-stack', ChangeSetType.CREATE); expect(result).toEqual({ - status: TemplateStatus.DEPLOYMENT_COMPLETE, - result: WorkflowResult.SUCCESSFUL, + phase: StackActionPhase.DEPLOYMENT_COMPLETE, + state: StackActionState.SUCCESSFUL, reason: undefined, }); expect(mockCfnService.waitUntilStackCreateComplete).toHaveBeenCalledWith({ @@ -112,8 +116,8 @@ describe('TemplateWorkflowOperations', () => { const result = await waitForDeployment(mockCfnService, 'test-stack', ChangeSetType.CREATE); expect(result).toEqual({ - status: TemplateStatus.DEPLOYMENT_FAILED, - result: WorkflowResult.FAILED, + phase: StackActionPhase.DEPLOYMENT_FAILED, + state: StackActionState.FAILED, reason: undefined, }); }); @@ -126,8 +130,8 @@ describe('TemplateWorkflowOperations', () => { const result = await waitForDeployment(mockCfnService, 'test-stack', ChangeSetType.UPDATE); expect(result).toEqual({ - status: TemplateStatus.DEPLOYMENT_COMPLETE, - result: WorkflowResult.SUCCESSFUL, + phase: StackActionPhase.DEPLOYMENT_COMPLETE, + state: StackActionState.SUCCESSFUL, reason: undefined, }); expect(mockCfnService.waitUntilStackUpdateComplete).toHaveBeenCalledWith({ @@ -141,13 +145,13 @@ describe('TemplateWorkflowOperations', () => { const { retryWithExponentialBackoff } = await import('../../../src/utils/Retry'); (retryWithExponentialBackoff as any).mockResolvedValue(undefined); - const workflow: TemplateWorkflowState = { + const workflow: StackActionWorkflowState = { id: 'test-id', changeSetName: 'changeset-123', stackName: 'test-stack', - status: TemplateStatus.VALIDATION_COMPLETE, + phase: StackActionPhase.VALIDATION_COMPLETE, startTime: Date.now(), - result: WorkflowResult.SUCCESSFUL, + state: StackActionState.SUCCESSFUL, }; await deleteStackAndChangeSet(mockCfnService, workflow, 'workflow-id'); @@ -169,13 +173,13 @@ describe('TemplateWorkflowOperations', () => { const { retryWithExponentialBackoff } = await import('../../../src/utils/Retry'); (retryWithExponentialBackoff as any).mockResolvedValue(undefined); - const workflow: TemplateWorkflowState = { + const workflow: StackActionWorkflowState = { id: 'test-id', changeSetName: 'changeset-123', stackName: 'test-stack', - status: TemplateStatus.VALIDATION_COMPLETE, + phase: StackActionPhase.VALIDATION_COMPLETE, startTime: Date.now(), - result: WorkflowResult.SUCCESSFUL, + state: StackActionState.SUCCESSFUL, }; await deleteChangeSet(mockCfnService, workflow, 'workflow-id'); @@ -192,8 +196,8 @@ describe('TemplateWorkflowOperations', () => { }); }); - describe('mapChangesToTemplateChanges', () => { - it('should map AWS SDK changes to template changes', () => { + describe('mapChangesToStackChanges', () => { + it('should map AWS SDK changes to stack changes', () => { const changes: Change[] = [ { Type: 'Resource', @@ -217,7 +221,7 @@ describe('TemplateWorkflowOperations', () => { }, ]; - const result = mapChangesToTemplateChanges(changes); + const result = mapChangesToStackChanges(changes); expect(result).toHaveLength(1); expect(result![0]).toEqual({ @@ -243,12 +247,12 @@ describe('TemplateWorkflowOperations', () => { }); it('should handle undefined changes', () => { - const result = mapChangesToTemplateChanges(undefined); + const result = mapChangesToStackChanges(undefined); expect(result).toBeUndefined(); }); it('should handle empty changes array', () => { - const result = mapChangesToTemplateChanges([]); + const result = mapChangesToStackChanges([]); expect(result).toEqual([]); }); }); @@ -273,8 +277,8 @@ describe('TemplateWorkflowOperations', () => { const result = await waitForValidation(mockCfnService, 'test-changeset', 'test-stack'); - expect(result.status).toBe(TemplateStatus.VALIDATION_COMPLETE); - expect(result.result).toBe(WorkflowResult.SUCCESSFUL); + expect(result.phase).toBe(StackActionPhase.VALIDATION_COMPLETE); + expect(result.state).toBe(StackActionState.SUCCESSFUL); expect(result.changes).toBeDefined(); expect(mockCfnService.waitUntilChangeSetCreateComplete).toHaveBeenCalledWith({ ChangeSetName: 'test-changeset', @@ -294,8 +298,8 @@ describe('TemplateWorkflowOperations', () => { const result = await waitForValidation(mockCfnService, 'test-changeset', 'test-stack'); - expect(result.status).toBe(TemplateStatus.VALIDATION_FAILED); - expect(result.result).toBe(WorkflowResult.FAILED); + expect(result.phase).toBe(StackActionPhase.VALIDATION_FAILED); + expect(result.state).toBe(StackActionState.FAILED); expect(result.reason).toBe('Test failure'); }); @@ -304,8 +308,8 @@ describe('TemplateWorkflowOperations', () => { const result = await waitForValidation(mockCfnService, 'test-changeset', 'test-stack'); - expect(result.status).toBe(TemplateStatus.VALIDATION_FAILED); - expect(result.result).toBe(WorkflowResult.FAILED); + expect(result.phase).toBe(StackActionPhase.VALIDATION_FAILED); + expect(result.state).toBe(StackActionState.FAILED); expect(result.reason).toBe('Network error'); }); @@ -314,8 +318,8 @@ describe('TemplateWorkflowOperations', () => { const result = await waitForValidation(mockCfnService, 'test-changeset', 'test-stack'); - expect(result.status).toBe(TemplateStatus.VALIDATION_FAILED); - expect(result.result).toBe(WorkflowResult.FAILED); + expect(result.phase).toBe(StackActionPhase.VALIDATION_FAILED); + expect(result.state).toBe(StackActionState.FAILED); expect(result.reason).toBe('String error'); }); }); diff --git a/tst/unit/templates/Validation.test.ts b/tst/unit/stackActions/Validation.test.ts similarity index 84% rename from tst/unit/templates/Validation.test.ts rename to tst/unit/stackActions/Validation.test.ts index 1ed3df91..599f9dc4 100644 --- a/tst/unit/templates/Validation.test.ts +++ b/tst/unit/stackActions/Validation.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach } from 'vitest'; -import { TemplateStatus } from '../../../src/templates/TemplateRequestType'; -import { Validation } from '../../../src/templates/Validation'; +import { StackActionPhase } from '../../../src/stacks/actions/StackActionRequestType'; +import { Validation } from '../../../src/stacks/actions/Validation'; describe('Validation', () => { let validation: Validation; @@ -31,9 +31,9 @@ describe('Validation', () => { expect(validationWithParams.getCapabilities()).toBe(capabilities); }); - it('should set and get status', () => { - validation.setStatus(TemplateStatus.VALIDATION_IN_PROGRESS); - expect(validation.getStatus()).toBe(TemplateStatus.VALIDATION_IN_PROGRESS); + it('should set and get phase', () => { + validation.setPhase(StackActionPhase.VALIDATION_IN_PROGRESS); + expect(validation.getPhase()).toBe(StackActionPhase.VALIDATION_IN_PROGRESS); }); it('should set and get changes', () => { @@ -65,8 +65,8 @@ describe('Validation', () => { expect(validation.getCapabilities()).toBeUndefined(); }); - it('should handle undefined status', () => { - expect(validation.getStatus()).toBeUndefined(); + it('should handle undefined phase', () => { + expect(validation.getPhase()).toBeUndefined(); }); it('should handle undefined changes', () => { @@ -94,9 +94,9 @@ describe('Validation', () => { expect(validationWithParams.getCapabilities()).toBe(capabilities); }); - it('should set and get status through consistent methods', () => { - validation.setStatus(TemplateStatus.VALIDATION_IN_PROGRESS); - expect(validation.getStatus()).toBe(TemplateStatus.VALIDATION_IN_PROGRESS); + it('should set and get phase through consistent methods', () => { + validation.setPhase(StackActionPhase.VALIDATION_IN_PROGRESS); + expect(validation.getPhase()).toBe(StackActionPhase.VALIDATION_IN_PROGRESS); }); it('should set and get changes through consistent methods', () => { @@ -121,7 +121,7 @@ describe('Validation', () => { }); it('should handle undefined values consistently', () => { - expect(validation.getStatus()).toBeUndefined(); + expect(validation.getPhase()).toBeUndefined(); expect(validation.getChanges()).toBeUndefined(); expect(validation.getCapabilities()).toBeUndefined(); }); diff --git a/tst/unit/templates/ValidationManager.test.ts b/tst/unit/stackActions/ValidationManager.test.ts similarity index 89% rename from tst/unit/templates/ValidationManager.test.ts rename to tst/unit/stackActions/ValidationManager.test.ts index 1821296d..12e3683c 100644 --- a/tst/unit/templates/ValidationManager.test.ts +++ b/tst/unit/stackActions/ValidationManager.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach } from 'vitest'; -import { Validation } from '../../../src/templates/Validation'; -import { ValidationManager } from '../../../src/templates/ValidationManager'; +import { Validation } from '../../../src/stacks/actions/Validation'; +import { ValidationManager } from '../../../src/stacks/actions/ValidationManager'; describe('ValidationManager', () => { let manager: ValidationManager; diff --git a/tst/unit/templates/ValidationWorkflow.test.ts b/tst/unit/stackActions/ValidationWorkflow.test.ts similarity index 89% rename from tst/unit/templates/ValidationWorkflow.test.ts rename to tst/unit/stackActions/ValidationWorkflow.test.ts index 38f48f4f..6b23cff2 100644 --- a/tst/unit/templates/ValidationWorkflow.test.ts +++ b/tst/unit/stackActions/ValidationWorkflow.test.ts @@ -3,11 +3,15 @@ import { SyntaxTreeManager } from '../../../src/context/syntaxtree/SyntaxTreeMan import { DocumentManager } from '../../../src/document/DocumentManager'; import { CfnService } from '../../../src/services/CfnService'; import { DiagnosticCoordinator } from '../../../src/services/DiagnosticCoordinator'; -import { TemplateActionParams, TemplateStatus, WorkflowResult } from '../../../src/templates/TemplateRequestType'; -import { processChangeSet } from '../../../src/templates/TemplateWorkflowOperations'; -import { ValidationWorkflow } from '../../../src/templates/ValidationWorkflow'; +import { processChangeSet } from '../../../src/stacks/actions/StackActionOperations'; +import { + CreateStackActionParams, + StackActionPhase, + StackActionState, +} from '../../../src/stacks/actions/StackActionRequestType'; +import { ValidationWorkflow } from '../../../src/stacks/actions/ValidationWorkflow'; -vi.mock('../../../src/templates/TemplateWorkflowOperations'); +vi.mock('../../../src/stacks/actions/StackActionOperations'); describe('ValidationWorkflow', () => { let validationWorkflow: ValidationWorkflow; @@ -35,7 +39,7 @@ describe('ValidationWorkflow', () => { describe('start', () => { it('should start validation workflow with CREATE when stack does not exist', async () => { - const params: TemplateActionParams = { + const params: CreateStackActionParams = { id: 'test-id', uri: 'file:///test.yaml', stackName: 'test-stack', @@ -56,7 +60,7 @@ describe('ValidationWorkflow', () => { }); it('should start validation workflow with UPDATE when stack exists', async () => { - const params: TemplateActionParams = { + const params: CreateStackActionParams = { id: 'test-id', uri: 'file:///test.yaml', stackName: 'test-stack', @@ -85,9 +89,9 @@ describe('ValidationWorkflow', () => { id: 'test-id', changeSetName: 'changeset-123', stackName: 'test-stack', - status: TemplateStatus.VALIDATION_IN_PROGRESS, + phase: StackActionPhase.VALIDATION_IN_PROGRESS, startTime: Date.now(), - result: WorkflowResult.IN_PROGRESS, + state: StackActionState.IN_PROGRESS, }; // Directly set workflow state @@ -96,8 +100,8 @@ describe('ValidationWorkflow', () => { const result = validationWorkflow.getStatus(params); expect(result).toEqual({ - status: TemplateStatus.VALIDATION_IN_PROGRESS, - result: WorkflowResult.IN_PROGRESS, + phase: StackActionPhase.VALIDATION_IN_PROGRESS, + state: StackActionState.IN_PROGRESS, changes: undefined, id: 'test-id', }); @@ -125,7 +129,7 @@ describe('ValidationWorkflow', () => { }); it('should add validation to manager when workflow starts', async () => { - const params: TemplateActionParams = { + const params: CreateStackActionParams = { id: 'test-id', uri: 'file:///test.yaml', stackName: 'test-stack', @@ -149,7 +153,7 @@ describe('ValidationWorkflow', () => { }); it('should get validation from manager during workflow operations', async () => { - const params: TemplateActionParams = { + const params: CreateStackActionParams = { id: 'test-id', uri: 'file:///test.yaml', stackName: 'test-stack', @@ -171,7 +175,7 @@ describe('ValidationWorkflow', () => { }); it('should remove validation from manager after workflow completion', async () => { - const params: TemplateActionParams = { + const params: CreateStackActionParams = { id: 'test-id', uri: 'file:///test.yaml', stackName: 'test-stack', diff --git a/tst/unit/templates/ValidationWorkflowEnhanced.test.ts b/tst/unit/stackActions/ValidationWorkflowEnhanced.test.ts similarity index 87% rename from tst/unit/templates/ValidationWorkflowEnhanced.test.ts rename to tst/unit/stackActions/ValidationWorkflowEnhanced.test.ts index 92463c26..ee560ce2 100644 --- a/tst/unit/templates/ValidationWorkflowEnhanced.test.ts +++ b/tst/unit/stackActions/ValidationWorkflowEnhanced.test.ts @@ -1,7 +1,7 @@ import { ChangeSetType } from '@aws-sdk/client-cloudformation'; import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { TemplateStatus, WorkflowResult } from '../../../src/templates/TemplateRequestType'; -import { ValidationWorkflow } from '../../../src/templates/ValidationWorkflow'; +import { StackActionPhase, StackActionState } from '../../../src/stacks/actions/StackActionRequestType'; +import { ValidationWorkflow } from '../../../src/stacks/actions/ValidationWorkflow'; describe('ValidationWorkflow Enhanced Features', () => { let workflow: ValidationWorkflow; @@ -54,9 +54,9 @@ describe('ValidationWorkflow Enhanced Features', () => { expect(addCall[0].getStackName()).toBe('test-stack'); }); - it('should update validation status on successful completion', async () => { + it('should update validation phase on successful completion', async () => { const mockValidation = { - setStatus: vi.fn(), + setPhase: vi.fn(), setChanges: vi.fn(), }; @@ -75,20 +75,20 @@ describe('ValidationWorkflow Enhanced Features', () => { id: 'test-id', changeSetName: 'test-changeset', stackName: 'test-stack', - status: TemplateStatus.VALIDATION_IN_PROGRESS, + phase: StackActionPhase.VALIDATION_IN_PROGRESS, startTime: Date.now(), - result: WorkflowResult.IN_PROGRESS, + state: StackActionState.IN_PROGRESS, }); await workflow['runValidationAsync']('test-id', 'test-changeset', 'test-stack', ChangeSetType.CREATE); - expect(mockValidation.setStatus).toHaveBeenCalledWith(TemplateStatus.VALIDATION_COMPLETE); + expect(mockValidation.setPhase).toHaveBeenCalledWith(StackActionPhase.VALIDATION_COMPLETE); expect(mockValidation.setChanges).toHaveBeenCalled(); }); it('should update validation status on failure', async () => { const mockValidation = { - setStatus: vi.fn(), + setPhase: vi.fn(), setChanges: vi.fn(), }; @@ -99,19 +99,19 @@ describe('ValidationWorkflow Enhanced Features', () => { id: 'test-id', changeSetName: 'test-changeset', stackName: 'test-stack', - status: TemplateStatus.VALIDATION_IN_PROGRESS, + phase: StackActionPhase.VALIDATION_IN_PROGRESS, startTime: Date.now(), - result: WorkflowResult.IN_PROGRESS, + state: StackActionState.IN_PROGRESS, }); await workflow['runValidationAsync']('test-id', 'test-changeset', 'test-stack', ChangeSetType.CREATE); - expect(mockValidation.setStatus).toHaveBeenCalledWith(TemplateStatus.VALIDATION_FAILED); + expect(mockValidation.setPhase).toHaveBeenCalledWith(StackActionPhase.VALIDATION_FAILED); }); it('should cleanup validation object after workflow completion', async () => { const mockValidation = { - setStatus: vi.fn(), + setPhase: vi.fn(), setChanges: vi.fn(), }; @@ -122,9 +122,9 @@ describe('ValidationWorkflow Enhanced Features', () => { id: 'test-id', changeSetName: 'test-changeset', stackName: 'test-stack', - status: TemplateStatus.VALIDATION_IN_PROGRESS, + phase: StackActionPhase.VALIDATION_IN_PROGRESS, startTime: Date.now(), - result: WorkflowResult.IN_PROGRESS, + state: StackActionState.IN_PROGRESS, }); await workflow['runValidationAsync']('test-id', 'test-changeset', 'test-stack', ChangeSetType.CREATE); @@ -135,7 +135,7 @@ describe('ValidationWorkflow Enhanced Features', () => { describe('Public API Integration Tests', () => { it('should handle complete workflow through public methods', async () => { const mockValidation = { - setStatus: vi.fn(), + setPhase: vi.fn(), setChanges: vi.fn(), }; @@ -175,7 +175,7 @@ describe('ValidationWorkflow Enhanced Features', () => { it('should handle workflow failure through public methods', async () => { const mockValidation = { - setStatus: vi.fn(), + setPhase: vi.fn(), setChanges: vi.fn(), }; diff --git a/tst/utils/MockServerComponents.ts b/tst/utils/MockServerComponents.ts index 13d7d125..cb0fcc6a 100644 --- a/tst/utils/MockServerComponents.ts +++ b/tst/utils/MockServerComponents.ts @@ -22,7 +22,6 @@ import { LspDocuments } from '../../src/protocol/LspDocuments'; import { LspHandlers } from '../../src/protocol/LspHandlers'; import { LspResourceHandlers } from '../../src/protocol/LspResourceHandlers'; import { LspStackHandlers } from '../../src/protocol/LspStackHandlers'; -import { LspTemplateHandlers } from '../../src/protocol/LspTemplateHandlers'; import { LspWorkspace } from '../../src/protocol/LspWorkspace'; import { ResourceStateImporter } from '../../src/resourceState/ResourceStateImporter'; import { ResourceStateManager } from '../../src/resourceState/ResourceStateManager'; @@ -41,9 +40,9 @@ import { GuardService } from '../../src/services/guard/GuardService'; import { IacGeneratorService } from '../../src/services/IacGeneratorService'; import { DefaultSettings, Settings } from '../../src/settings/Settings'; import { SettingsManager } from '../../src/settings/SettingsManager'; +import { DeploymentWorkflow } from '../../src/stacks/actions/DeploymentWorkflow'; +import { ValidationWorkflow } from '../../src/stacks/actions/ValidationWorkflow'; import { ClientMessage } from '../../src/telemetry/ClientMessage'; -import { DeploymentWorkflow } from '../../src/templates/DeploymentWorkflow'; -import { ValidationWorkflow } from '../../src/templates/ValidationWorkflow'; export class MockedServerComponents extends ServerComponents { declare readonly diagnostics: StubbedInstance; @@ -106,11 +105,7 @@ export function createMockLspResourceHandlers() { return stubInterface(); } -export function createMockLspTemplateHandlers() { - return stubInterface(); -} - -export function createMockStackHandlers() { +export function createMockLspStackHandlers() { return stubInterface(); }