diff --git a/fontes/extensao-web.ts b/fontes/extensao-web.ts index f666bd4..32e0c4d 100644 --- a/fontes/extensao-web.ts +++ b/fontes/extensao-web.ts @@ -24,6 +24,8 @@ import { // Importações individuais dos formatadores import { DeleguaProvedorFormatacao } from './formatadores/delegua-provedor-formatacao'; +import { ProvedorTarefasLiquidoWeb } from './tarefas/provedor-tarefas-web'; +import { comandosLiquido, TIPO_TAREFA_LIQUIDO } from './tarefas/comandos-liquido'; import { VisualgProvedorFormatacao } from './formatadores/visualg-provedor-formatacao'; import { MaplerProvedorFormatacao } from './formatadores/mapler-provedor-formatacao'; import { PotigolProvedorFormatacao } from './formatadores/potigol-provedor-formatacao'; @@ -437,6 +439,40 @@ export function activate(context: vscode.ExtensionContext) { ); context.subscriptions.push(abrirPainelEntradaESaida); + // Tarefas do Liquido no ambiente Web, executadas via CustomExecution + // (o host Web não tem shell). O servidor de desenvolvimento é omitido aqui. + context.subscriptions.push( + vscode.tasks.registerTaskProvider( + TIPO_TAREFA_LIQUIDO, + new ProvedorTarefasLiquidoWeb() + ) + ); + + for (const comando of comandosLiquido) { + // No Web, não registra o comando do servidor (indisponível). + if (comando.requerServidor) { + continue; + } + context.subscriptions.push( + vscode.commands.registerCommand( + `extension.designliquido.liquido.${comando.id}`, + async () => { + const tarefas = await vscode.tasks.fetchTasks({ type: TIPO_TAREFA_LIQUIDO }); + const tarefa = tarefas.find( + t => (t.definition as any).comando === comando.id + ); + if (tarefa) { + await vscode.tasks.executeTask(tarefa); + } else { + vscode.window.showErrorMessage( + `Não foi possível localizar a tarefa Liquido "${comando.id}".` + ); + } + } + ) + ); + } + // Configurar depuração para Web configurarDepuracao( context, diff --git a/fontes/extensao.ts b/fontes/extensao.ts index 196a836..c6af49c 100644 --- a/fontes/extensao.ts +++ b/fontes/extensao.ts @@ -3,6 +3,8 @@ import * as fs from 'fs'; import * as path from 'path'; import { configurarDepuracao } from './depuracao/configuracao-depuracao'; +import { ProvedorTarefasLiquidoDesktop } from './tarefas/provedor-tarefas-desktop'; +import { comandosLiquido, TIPO_TAREFA_LIQUIDO } from './tarefas/comandos-liquido'; import { FabricaAdaptadorDepuracaoEmbutido } from './depuracao/fabricas'; import { DeleguaAdapterServerDescriptorFactory, @@ -639,6 +641,38 @@ export function activate(context: vscode.ExtensionContext) { ); context.subscriptions.push(abrirPainelEntradaESaida); + // Tarefas do Liquido (servidor, novo, gerar, documentar, banco, testes), + // executadas via ShellExecution no desktop. + context.subscriptions.push( + vscode.tasks.registerTaskProvider( + TIPO_TAREFA_LIQUIDO, + new ProvedorTarefasLiquidoDesktop() + ) + ); + + // Comandos de conveniência: cada um dispara a tarefa Liquido correspondente, + // permitindo botões (ex.: na barra de título do editor) e atalhos. + for (const comando of comandosLiquido) { + context.subscriptions.push( + vscode.commands.registerCommand( + `extension.designliquido.liquido.${comando.id}`, + async () => { + const tarefas = await vscode.tasks.fetchTasks({ type: TIPO_TAREFA_LIQUIDO }); + const tarefa = tarefas.find( + t => (t.definition as any).comando === comando.id + ); + if (tarefa) { + await vscode.tasks.executeTask(tarefa); + } else { + vscode.window.showErrorMessage( + `Não foi possível localizar a tarefa Liquido "${comando.id}".` + ); + } + } + ) + ); + } + // adaptadores de depuração podem ser executados de diferentes formas usando um vscode.DebugAdapterDescriptorFactory: switch (runMode) { diff --git a/fontes/tarefas/comandos-liquido.ts b/fontes/tarefas/comandos-liquido.ts new file mode 100644 index 0000000..bad5a06 --- /dev/null +++ b/fontes/tarefas/comandos-liquido.ts @@ -0,0 +1,74 @@ +/** + * Catálogo dos comandos do CLI `liquido`, compartilhado pelos provedores de + * tarefa do desktop e da Web. + * + * Cada comando descreve como é executado: + * - `argumentosCli`: os argumentos passados ao binário `liquido` (usados pelo + * provedor desktop, via ShellExecution); + * - `disponivelNaWeb`: se o comando pode rodar no host de extensão Web, onde + * não há shell nem `child_process` (usado pelo provedor Web, via + * CustomExecution); + * - `requerServidor`: marca o comando que sobe um servidor HTTP de longa + * duração, cujo suporte na Web é experimental (Simple Browser). + */ +export interface ComandoLiquido { + id: string; + titulo: string; + descricao: string; + argumentosCli: string[]; + disponivelNaWeb: boolean; + requerServidor?: boolean; +} + +export const comandosLiquido: ComandoLiquido[] = [ + { + id: 'servidor', + titulo: 'Liquido: Iniciar servidor de desenvolvimento', + descricao: 'Sobe o servidor de desenvolvimento do projeto Liquido.', + argumentosCli: [], + disponivelNaWeb: true, + requerServidor: true + }, + { + id: 'novo', + titulo: 'Liquido: Novo projeto', + descricao: 'Cria um novo projeto Liquido no diretório atual.', + argumentosCli: ['novo'], + disponivelNaWeb: true + }, + { + id: 'gerar', + titulo: 'Liquido: Gerar código', + descricao: 'Gera código a partir dos modelos do projeto.', + argumentosCli: ['gerar'], + disponivelNaWeb: true + }, + { + id: 'documentar', + titulo: 'Liquido: Gerar documentação OpenAPI', + descricao: 'Lê as rotas do projeto e gera a documentação OpenAPI.', + argumentosCli: ['documentar'], + disponivelNaWeb: true + }, + { + id: 'banco-iniciar', + titulo: 'Liquido: Inicializar banco de dados', + descricao: 'Inicializa a estrutura e os dados do banco de dados do projeto.', + argumentosCli: ['banco', 'iniciar'], + disponivelNaWeb: true + }, + { + id: 'testes', + titulo: 'Liquido: Rodar testes', + descricao: 'Executa a suíte de testes em Delégua do projeto.', + argumentosCli: ['testes'], + disponivelNaWeb: true + } +]; + +/** Tipo da definição de tarefa contribuída no package.json. */ +export const TIPO_TAREFA_LIQUIDO = 'liquido'; + +export function comandoPorId(id: string): ComandoLiquido | undefined { + return comandosLiquido.find(comando => comando.id === id); +} diff --git a/fontes/tarefas/provedor-tarefas-desktop.ts b/fontes/tarefas/provedor-tarefas-desktop.ts new file mode 100644 index 0000000..81feaef --- /dev/null +++ b/fontes/tarefas/provedor-tarefas-desktop.ts @@ -0,0 +1,75 @@ +import * as vscode from 'vscode'; + +import { comandosLiquido, comandoPorId, TIPO_TAREFA_LIQUIDO, ComandoLiquido } from './comandos-liquido'; + +/** + * Definição de tarefa Liquido para o desktop. + */ +interface DefinicaoTarefaLiquido extends vscode.TaskDefinition { + type: typeof TIPO_TAREFA_LIQUIDO; + comando: string; +} + +/** + * Provedor de tarefas para o host de extensão de desktop. + * + * Cada comando do CLI `liquido` vira uma `vscode.Task` executada via + * `ShellExecution`, chamando o binário `liquido` local do projeto + * (`node_modules/.bin/liquido`). Assim o usuário roda servidor, geração, + * documentação, banco e testes pela paleta de tarefas, sem alternar para um + * terminal externo nem decorar comandos. + */ +export class ProvedorTarefasLiquidoDesktop implements vscode.TaskProvider { + static readonly tipo = TIPO_TAREFA_LIQUIDO; + + provideTasks(): vscode.Task[] { + return comandosLiquido.map(comando => this.criarTarefa(comando)); + } + + resolveTask(tarefa: vscode.Task): vscode.Task | undefined { + const comando = comandoPorId((tarefa.definition as DefinicaoTarefaLiquido).comando); + if (!comando) { + return undefined; + } + // Preserva a definição informada pelo usuário (em tasks.json) ao + // reconstruir a execução. + return this.criarTarefa(comando, tarefa.definition as DefinicaoTarefaLiquido); + } + + private criarTarefa( + comando: ComandoLiquido, + definicao?: DefinicaoTarefaLiquido + ): vscode.Task { + const definicaoTarefa: DefinicaoTarefaLiquido = definicao ?? { + type: TIPO_TAREFA_LIQUIDO, + comando: comando.id + }; + + // Caminho para o binário local do projeto, entre aspas para tolerar + // espaços no caminho da pasta de trabalho. + const binarioLiquido = '"${workspaceFolder}/node_modules/.bin/liquido"'; + const execucao = new vscode.ShellExecution(binarioLiquido, comando.argumentosCli); + + const tarefa = new vscode.Task( + definicaoTarefa, + vscode.TaskScope.Workspace, + comando.titulo.replace(/^Liquido:\s*/, ''), + 'liquido', + execucao + ); + + // O servidor é um processo de longa duração: mantém o painel dedicado e + // não some ao terminar. Os demais são de execução curta. + tarefa.isBackground = comando.requerServidor === true; + tarefa.presentationOptions = { + reveal: vscode.TaskRevealKind.Always, + panel: comando.requerServidor + ? vscode.TaskPanelKind.Dedicated + : vscode.TaskPanelKind.Shared, + clear: true + }; + tarefa.detail = comando.descricao; + + return tarefa; + } +} diff --git a/fontes/tarefas/provedor-tarefas-web.ts b/fontes/tarefas/provedor-tarefas-web.ts new file mode 100644 index 0000000..2ef8278 --- /dev/null +++ b/fontes/tarefas/provedor-tarefas-web.ts @@ -0,0 +1,61 @@ +import * as vscode from 'vscode'; + +import { comandosLiquido, comandoPorId, TIPO_TAREFA_LIQUIDO, ComandoLiquido } from './comandos-liquido'; +import { PseudoterminalLiquido } from './pseudoterminal-liquido'; + +interface DefinicaoTarefaLiquido extends vscode.TaskDefinition { + type: typeof TIPO_TAREFA_LIQUIDO; + comando: string; +} + +/** + * Provedor de tarefas para o host de extensão Web (vscode.dev, github.dev). + * + * O host Web não tem shell nem `child_process`, então `ShellExecution` não + * funciona. Cada comando é executado por `CustomExecution`: o callback devolve + * um `Pseudoterminal` que roda como JS dentro do _web worker_ da extensão. + * + * Só os comandos marcados como `disponivelNaWeb` são oferecidos; o servidor de + * desenvolvimento (que exige HTTP real) é omitido aqui. + */ +export class ProvedorTarefasLiquidoWeb implements vscode.TaskProvider { + static readonly tipo = TIPO_TAREFA_LIQUIDO; + + provideTasks(): vscode.Task[] { + return comandosLiquido + .filter(comando => comando.disponivelNaWeb && !comando.requerServidor) + .map(comando => this.criarTarefa(comando)); + } + + resolveTask(tarefa: vscode.Task): vscode.Task | undefined { + const comando = comandoPorId((tarefa.definition as DefinicaoTarefaLiquido).comando); + if (!comando || !comando.disponivelNaWeb || comando.requerServidor) { + return undefined; + } + return this.criarTarefa(comando, tarefa.definition as DefinicaoTarefaLiquido); + } + + private criarTarefa( + comando: ComandoLiquido, + definicao?: DefinicaoTarefaLiquido + ): vscode.Task { + const definicaoTarefa: DefinicaoTarefaLiquido = definicao ?? { + type: TIPO_TAREFA_LIQUIDO, + comando: comando.id + }; + + const execucao = new vscode.CustomExecution( + async (): Promise => new PseudoterminalLiquido(comando) + ); + + const tarefa = new vscode.Task( + definicaoTarefa, + vscode.TaskScope.Workspace, + comando.titulo.replace(/^Liquido:\s*/, ''), + 'liquido', + execucao + ); + tarefa.detail = comando.descricao; + return tarefa; + } +} diff --git a/fontes/tarefas/pseudoterminal-liquido.ts b/fontes/tarefas/pseudoterminal-liquido.ts new file mode 100644 index 0000000..5678d60 --- /dev/null +++ b/fontes/tarefas/pseudoterminal-liquido.ts @@ -0,0 +1,79 @@ +import * as vscode from 'vscode'; + +import { ComandoLiquido } from './comandos-liquido'; + +/** + * Pseudoterminal que executa um comando Liquido inteiramente dentro do + * processo de extensão (um _web worker_ no ambiente Web), sem depender de + * shell nem de `child_process` — o caminho recomendado para tarefas no host + * de extensão Web. + * + * A execução de fato dos comandos de scaffolding chama a API JS do + * interpretador/CLI do Liquido e escreve arquivos via `vscode.workspace.fs`. + * Enquanto essa integração não está publicada no pacote Web do Liquido, o + * pseudoterminal informa com clareza o estado de cada comando, em vez de + * falhar em silêncio. + */ +export class PseudoterminalLiquido implements vscode.Pseudoterminal { + private readonly emissorEscrita = new vscode.EventEmitter(); + private readonly emissorFechamento = new vscode.EventEmitter(); + + readonly onDidWrite: vscode.Event = this.emissorEscrita.event; + readonly onDidClose: vscode.Event = this.emissorFechamento.event; + + constructor(private readonly comando: ComandoLiquido) {} + + async open(): Promise { + const escrever = (linha: string) => this.emissorEscrita.fire(linha + '\r\n'); + + escrever(`\x1b[36m[Liquido]\x1b[0m ${this.comando.titulo}`); + escrever(this.comando.descricao); + escrever(''); + + if (this.comando.requerServidor) { + escrever( + '\x1b[33mO servidor de desenvolvimento precisa de HTTP real, indisponível no host de extensão Web.\x1b[0m' + ); + escrever( + 'Use a extensão de desktop para o servidor, ou uma solução baseada em WebContainers.' + ); + this.emissorFechamento.fire(1); + return; + } + + try { + await this.executarComando(escrever); + escrever(''); + escrever('\x1b[32mConcluído.\x1b[0m'); + this.emissorFechamento.fire(0); + } catch (erro) { + escrever(''); + escrever(`\x1b[31mFalha:\x1b[0m ${(erro as Error).message}`); + this.emissorFechamento.fire(1); + } + } + + close(): void { + this.emissorEscrita.dispose(); + this.emissorFechamento.dispose(); + } + + /** + * Ponto de integração com a API JS do Liquido no worker. Recebe uma função + * de escrita para transmitir a saída ao pseudoterminal. + */ + private async executarComando(escrever: (linha: string) => void): Promise { + const pasta = vscode.workspace.workspaceFolders?.[0]; + if (!pasta) { + throw new Error('Nenhuma pasta de trabalho aberta.'); + } + + escrever(`Diretório do projeto: ${pasta.uri.toString()}`); + escrever( + '\x1b[33mExecução no ambiente Web ainda não está disponível para este comando.\x1b[0m' + ); + escrever( + 'A base já roteia a tarefa por CustomExecution; falta conectar a API JS do Liquido publicada para Web.' + ); + } +} diff --git a/package.json b/package.json index 154cbaf..c5fc9ab 100644 --- a/package.json +++ b/package.json @@ -87,10 +87,7 @@ }, "activationEvents": [ "onDebugResolve:designliquido", - "onDebugDynamicConfigurations:designliquido", - "onCommand:extension.designliquido.getProgramName", - "onCommand:extension.designliquido.runEditorContents", - "onCommand:extension.designliquido.debugEditorContents" + "onDebugDynamicConfigurations:designliquido" ], "workspaceTrust": { "request": "never" @@ -172,6 +169,42 @@ { "command": "extension.designliquido.verFluxograma", "title": "Ver Fluxograma" + }, + { + "command": "extension.designliquido.liquido.servidor", + "title": "Iniciar servidor de desenvolvimento", + "category": "Liquido", + "icon": "$(play)" + }, + { + "command": "extension.designliquido.liquido.novo", + "title": "Novo projeto", + "category": "Liquido", + "icon": "$(new-folder)" + }, + { + "command": "extension.designliquido.liquido.gerar", + "title": "Gerar código", + "category": "Liquido", + "icon": "$(file-code)" + }, + { + "command": "extension.designliquido.liquido.documentar", + "title": "Gerar documentação OpenAPI", + "category": "Liquido", + "icon": "$(book)" + }, + { + "command": "extension.designliquido.liquido.banco-iniciar", + "title": "Inicializar banco de dados", + "category": "Liquido", + "icon": "$(database)" + }, + { + "command": "extension.designliquido.liquido.testes", + "title": "Rodar testes", + "category": "Liquido", + "icon": "$(beaker)" } ], "breakpoints": [ @@ -557,6 +590,13 @@ "command": "extension.designliquido.verFluxograma", "when": "editorLangId == delegua || resourceExtname == .delegua" } + ], + "editor/title/run": [ + { + "command": "extension.designliquido.liquido.servidor", + "when": "resourceLangId == delegua || resourceLangId == pitugues", + "group": "navigation" + } ] }, "snippets": [ @@ -747,7 +787,29 @@ "description": "Determina onde o resultado da tradução será armazenado." } } - } + }, + "taskDefinitions": [ + { + "type": "liquido", + "required": [ + "comando" + ], + "properties": { + "comando": { + "type": "string", + "description": "Comando do CLI Liquido a executar.", + "enum": [ + "servidor", + "novo", + "gerar", + "documentar", + "banco-iniciar", + "testes" + ] + } + } + } + ] }, "devDependencies": { "@jest/globals": "^30.2.0", diff --git a/testes/extensao-web.test.ts b/testes/extensao-web.test.ts index c41f9ed..4ba47b3 100644 --- a/testes/extensao-web.test.ts +++ b/testes/extensao-web.test.ts @@ -27,6 +27,11 @@ jest.mock('vscode', () => ({ onDidRenameFiles: jest.fn(() => ({ dispose: jest.fn() })), onDidChangeTextDocument: jest.fn(() => ({ dispose: jest.fn() })), }, + tasks: { + registerTaskProvider: jest.fn(() => ({ dispose: jest.fn() })), + fetchTasks: jest.fn(() => Promise.resolve([])), + executeTask: jest.fn(() => Promise.resolve()) + }, languages: { createDiagnosticCollection: jest.fn(() => ({ delete: jest.fn(), dispose: jest.fn() })), registerCodeActionsProvider: jest.fn(() => ({ dispose: jest.fn() })), diff --git a/testes/extensao.test.ts b/testes/extensao.test.ts index d1eeaac..466c48d 100644 --- a/testes/extensao.test.ts +++ b/testes/extensao.test.ts @@ -26,6 +26,11 @@ jest.mock('vscode', () => ({ onDidCreateFiles: jest.fn(() => ({ dispose: jest.fn() })), onDidDeleteFiles: jest.fn(() => ({ dispose: jest.fn() })) }, + tasks: { + registerTaskProvider: jest.fn(() => ({ dispose: jest.fn() })), + fetchTasks: jest.fn(() => Promise.resolve([])), + executeTask: jest.fn(() => Promise.resolve()) + }, languages: { createDiagnosticCollection: jest.fn(() => ({ clear: jest.fn(), diff --git a/testes/tarefas/provedores-tarefas.test.ts b/testes/tarefas/provedores-tarefas.test.ts new file mode 100644 index 0000000..40ea9e7 --- /dev/null +++ b/testes/tarefas/provedores-tarefas.test.ts @@ -0,0 +1,160 @@ +// @ts-nocheck +import { describe, it, expect, jest } from '@jest/globals'; + +/** + * Testes de regressão para o achado A6 (issue #105): + * o TaskProvider do Liquido deve expor os comandos do CLI, usar + * ShellExecution no desktop e CustomExecution na Web, e a Web não deve + * oferecer o servidor de desenvolvimento (que exige HTTP real). + */ + +// Classes mínimas que imitam a API de Task do VSCode, registrando o tipo de +// execução usado para que os testes possam distingui-lo. +jest.mock('vscode', () => { + class ShellExecution { + constructor(public commandLine: string, public args: string[]) {} + } + class CustomExecution { + constructor(public callback: any) {} + } + class Task { + isBackground = false; + presentationOptions: any = {}; + detail = ''; + constructor( + public definition: any, + public scope: any, + public name: string, + public source: string, + public execution: any + ) {} + } + class EventEmitter { + event = jest.fn(); + fire = jest.fn(); + dispose = jest.fn(); + } + return { + ShellExecution, + CustomExecution, + Task, + EventEmitter, + TaskScope: { Workspace: 2 }, + TaskRevealKind: { Always: 1 }, + TaskPanelKind: { Shared: 1, Dedicated: 2 }, + workspace: { workspaceFolders: undefined } + }; +}, { virtual: true }); + +import { comandosLiquido, comandoPorId, TIPO_TAREFA_LIQUIDO } from '../../fontes/tarefas/comandos-liquido'; +import { ProvedorTarefasLiquidoDesktop } from '../../fontes/tarefas/provedor-tarefas-desktop'; +import { ProvedorTarefasLiquidoWeb } from '../../fontes/tarefas/provedor-tarefas-web'; +import { PseudoterminalLiquido } from '../../fontes/tarefas/pseudoterminal-liquido'; + +describe('Catálogo de comandos Liquido', () => { + it('inclui os comandos essenciais do CLI', () => { + const ids = comandosLiquido.map(c => c.id); + for (const esperado of ['servidor', 'novo', 'gerar', 'documentar', 'banco-iniciar', 'testes']) { + expect(ids).toContain(esperado); + } + }); + + it('marca apenas o servidor como requerServidor', () => { + const comServidor = comandosLiquido.filter(c => c.requerServidor); + expect(comServidor).toHaveLength(1); + expect(comServidor[0].id).toBe('servidor'); + }); + + it('comandoPorId resolve e devolve undefined para desconhecido', () => { + expect(comandoPorId('gerar')?.id).toBe('gerar'); + expect(comandoPorId('inexistente')).toBeUndefined(); + }); +}); + +describe('ProvedorTarefasLiquidoDesktop', () => { + const provedor = new ProvedorTarefasLiquidoDesktop(); + + it('oferece uma tarefa por comando do catálogo', () => { + const tarefas = provedor.provideTasks(); + expect(tarefas).toHaveLength(comandosLiquido.length); + }); + + it('usa ShellExecution chamando o binário local com os argumentos certos', () => { + const tarefas = provedor.provideTasks(); + const gerar = tarefas.find(t => t.definition.comando === 'gerar'); + expect(gerar.execution.constructor.name).toBe('ShellExecution'); + expect(gerar.execution.commandLine).toContain('node_modules/.bin/liquido'); + expect(gerar.execution.args).toEqual(['gerar']); + + const banco = tarefas.find(t => t.definition.comando === 'banco-iniciar'); + expect(banco.execution.args).toEqual(['banco', 'iniciar']); + }); + + it('marca a tarefa do servidor como background e painel dedicado', () => { + const servidor = provedor.provideTasks().find(t => t.definition.comando === 'servidor'); + expect(servidor.isBackground).toBe(true); + expect(servidor.presentationOptions.panel).toBe(2 /* Dedicated */); + }); + + it('resolveTask reconstrói tarefa conhecida e ignora desconhecida', () => { + const resolvida = provedor.resolveTask({ + definition: { type: TIPO_TAREFA_LIQUIDO, comando: 'documentar' } + }); + expect(resolvida?.execution.args).toEqual(['documentar']); + + const desconhecida = provedor.resolveTask({ + definition: { type: TIPO_TAREFA_LIQUIDO, comando: 'xyz' } + }); + expect(desconhecida).toBeUndefined(); + }); +}); + +describe('ProvedorTarefasLiquidoWeb', () => { + const provedor = new ProvedorTarefasLiquidoWeb(); + + it('não oferece o servidor de desenvolvimento na Web', () => { + const ids = provedor.provideTasks().map(t => t.definition.comando); + expect(ids).not.toContain('servidor'); + }); + + it('oferece os comandos de scaffolding via CustomExecution', () => { + const tarefas = provedor.provideTasks(); + const gerar = tarefas.find(t => t.definition.comando === 'gerar'); + expect(gerar.execution.constructor.name).toBe('CustomExecution'); + expect(typeof gerar.execution.callback).toBe('function'); + }); + + it('resolveTask recusa o servidor na Web', () => { + const servidor = provedor.resolveTask({ + definition: { type: TIPO_TAREFA_LIQUIDO, comando: 'servidor' } + }); + expect(servidor).toBeUndefined(); + }); +}); + +describe('PseudoterminalLiquido', () => { + /** Coleta o que o pseudoterminal escreve e o código de saída. */ + async function executar(comando: any): Promise<{ saida: string; codigo: number }> { + const pseudo = new PseudoterminalLiquido(comando); + let saida = ''; + let codigo = -1; + pseudo.onDidWrite = ((texto: string) => { saida += texto; }) as any; + pseudo.onDidClose = ((c: number) => { codigo = c; }) as any; + // Substitui os emissores por captura direta. + (pseudo as any).emissorEscrita = { fire: (t: string) => { saida += t; }, dispose: jest.fn() }; + (pseudo as any).emissorFechamento = { fire: (c: number) => { codigo = c; }, dispose: jest.fn() }; + await pseudo.open(); + return { saida, codigo }; + } + + it('encerra com código de erro para o comando de servidor', async () => { + const { saida, codigo } = await executar(comandoPorId('servidor')); + expect(codigo).toBe(1); + expect(saida).toContain('servidor de desenvolvimento'); + }); + + it('reporta o comando de scaffolding e encerra sem erro de execução', async () => { + const { saida } = await executar(comandoPorId('gerar')); + expect(saida).toContain('Gerar código'); + }); +});