From 619044b9bba9c9bd9712cb0a58a1d758427c2e43 Mon Sep 17 00:00:00 2001 From: Oxbar - AKA Micael Date: Mon, 27 Jul 2026 12:23:43 -0300 Subject: [PATCH] Adiciona TaskProvider para executar/testar projetos Liquido pelo editor (#105) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A extensão não tinha como executar um projeto Liquido a partir do editor: sem TaskProvider, sem terminal, sem botão. Todo o ciclo dependia de um terminal externo. Havia ainda três activationEvents apontando para comandos inexistentes (getProgramName, runEditorContents, debugEditorContents). Este commit adiciona tarefas para os comandos do CLI liquido (servidor, novo, gerar, documentar, banco iniciar, testes), com estratégias distintas por ambiente, seguindo a orientação de viabilidade Web da issue: - Desktop (fontes/tarefas/provedor-tarefas-desktop.ts): ShellExecution chamando o binário local node_modules/.bin/liquido. O servidor é marcado como tarefa de fundo, em painel dedicado; os demais são de execução curta. - Web (fontes/tarefas/provedor-tarefas-web.ts): o host Web não tem shell nem child_process, então usa CustomExecution — o callback devolve um Pseudoterminal que roda como JS no web worker (fontes/tarefas/pseudoterminal-liquido.ts). O servidor de desenvolvimento, que exige HTTP real, é omitido no Web. - Catálogo compartilhado (fontes/tarefas/comandos-liquido.ts) descreve cada comando, seus argumentos de CLI e sua disponibilidade por ambiente. Contribuições no package.json: taskDefinitions do tipo 'liquido', seis comandos de conveniência (extension.designliquido.liquido.*) que disparam a tarefa correspondente, e um botão de iniciar servidor na barra de título do editor para arquivos Delégua e Pituguês. Os três activationEvents órfãos foram removidos. Escopo honesto sobre o ambiente Web: a base já roteia as tarefas de scaffolding por CustomExecution e o servidor é corretamente tratado como indisponível no Web, mas a execução de fato dos comandos de scaffolding dentro do worker depende da API JS do Liquido publicada para Web, ainda não disponível — o pseudoterminal reporta esse estado com clareza, em vez de falhar em silêncio. A evolução do servidor no Web (Service Worker interceptando fetch, ou WebContainers) fica para um passo seguinte. Testes: novo testes/tarefas/provedores-tarefas.test.ts (12 casos) cobrindo o catálogo, o provedor desktop (ShellExecution com os argumentos corretos, servidor como background/painel dedicado, resolveTask), o provedor Web (CustomExecution, servidor omitido) e o pseudoterminal. Mock de vscode.tasks adicionado aos testes de extensão. Fixes #105 --- fontes/extensao-web.ts | 36 +++++ fontes/extensao.ts | 34 +++++ fontes/tarefas/comandos-liquido.ts | 74 ++++++++++ fontes/tarefas/provedor-tarefas-desktop.ts | 75 ++++++++++ fontes/tarefas/provedor-tarefas-web.ts | 61 ++++++++ fontes/tarefas/pseudoterminal-liquido.ts | 79 ++++++++++ package.json | 72 +++++++++- testes/extensao-web.test.ts | 5 + testes/extensao.test.ts | 5 + testes/tarefas/provedores-tarefas.test.ts | 160 +++++++++++++++++++++ 10 files changed, 596 insertions(+), 5 deletions(-) create mode 100644 fontes/tarefas/comandos-liquido.ts create mode 100644 fontes/tarefas/provedor-tarefas-desktop.ts create mode 100644 fontes/tarefas/provedor-tarefas-web.ts create mode 100644 fontes/tarefas/pseudoterminal-liquido.ts create mode 100644 testes/tarefas/provedores-tarefas.test.ts 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'); + }); +});