From e0fd1d6fbd38d4daef43c66a584bb18595608261 Mon Sep 17 00:00:00 2001 From: dsorajisto <103614897+dsorajisto@users.noreply.github.com> Date: Tue, 2 Jun 2026 00:27:59 -0400 Subject: [PATCH] test(language/lint): add unit tests for empty-block pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds 5 unit tests for the empty-block lint pass in packages/language/src/lint/empty-block.ts, which flags `inputs:` or `outputs:` blocks that have no entries. Coverage: - Bare `inputs:` and `outputs:` (null-ish value) → flagged - Populated blocks → not flagged - Both blocks empty → two diagnostics - Multiple actions → one diagnostic per affected action Follows the same test pattern as required-fields.test.ts (#38). All tests pass via: pnpm --filter @agentscript/language test empty-block --- .../language/src/lint/empty-block.test.ts | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 packages/language/src/lint/empty-block.test.ts diff --git a/packages/language/src/lint/empty-block.test.ts b/packages/language/src/lint/empty-block.test.ts new file mode 100644 index 00000000..2e25b19e --- /dev/null +++ b/packages/language/src/lint/empty-block.test.ts @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2026, Salesforce, Inc. + * All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * For full license text, see the LICENSE file in the repo root or https://www.apache.org/licenses/LICENSE-2.0 + */ + +import { describe, it, expect } from 'vitest'; +import { parse } from '@agentscript/parser'; +import { Dialect } from '../core/dialect.js'; +import { LintEngine } from '../core/analysis/lint-engine.js'; +import { createSchemaContext } from '../core/analysis/scope.js'; +import { ActionsBlock } from '../blocks.js'; +import { emptyBlockPass } from './empty-block.js'; + +const TestSchema = { + actions: ActionsBlock, +}; + +const schemaCtx = createSchemaContext({ schema: TestSchema, aliases: {} }); + +function getDiagnostics(source: string, code?: string) { + const { rootNode: root } = parse(source); + const mappingNode = + root.namedChildren.find(n => n.type === 'mapping') ?? root; + + const dialect = new Dialect(); + const result = dialect.parse(mappingNode, TestSchema); + + const engine = new LintEngine({ + passes: [emptyBlockPass()], + source: 'test', + }); + const { diagnostics } = engine.run(result.value, schemaCtx); + if (!code) return diagnostics; + return diagnostics.filter(d => d.code === code); +} + +describe('empty-block lint pass', () => { + it('flags a bare `inputs:` with no entries', () => { + const diags = getDiagnostics( + ` +actions: + Lookup: + description: "Look something up" + target: "flow://Lookup" + inputs: +`, + 'empty-block' + ); + expect(diags).toHaveLength(1); + expect(diags[0].message).toContain('inputs'); + }); + + it('does not flag a populated `inputs:` block', () => { + const diags = getDiagnostics( + ` +actions: + Lookup: + description: "Look something up" + target: "flow://Lookup" + inputs: + query: string +`, + 'empty-block' + ); + expect(diags).toHaveLength(0); + }); + + it('flags a bare `outputs:` with no entries', () => { + const diags = getDiagnostics( + ` +actions: + Lookup: + description: "Look something up" + target: "flow://Lookup" + outputs: +`, + 'empty-block' + ); + expect(diags).toHaveLength(1); + expect(diags[0].message).toContain('outputs'); + }); + + it('reports two diagnostics when both `inputs:` and `outputs:` are empty', () => { + const diags = getDiagnostics( + ` +actions: + Lookup: + description: "x" + target: "flow://Lookup" + inputs: + outputs: +`, + 'empty-block' + ); + expect(diags).toHaveLength(2); + }); + + it('reports one diagnostic per action when multiple actions have empty blocks', () => { + const diags = getDiagnostics( + ` +actions: + Alpha: + description: "a" + target: "flow://Alpha" + inputs: + Beta: + description: "b" + target: "flow://Beta" + inputs: + x: string + Gamma: + description: "g" + target: "flow://Gamma" + outputs: +`, + 'empty-block' + ); + expect(diags).toHaveLength(2); + }); +});