From e57b0ebd8b009d59077445a725ba92a00499c9db Mon Sep 17 00:00:00 2001 From: "jasmine.kaur" Date: Tue, 21 Apr 2026 10:32:25 +0530 Subject: [PATCH] Add-skill-test-examples --- tests/README.md | 239 ++++++++++++++++++ .../script-tests/references.test.js | 22 ++ .../script-tests/templates.test.ts | 26 ++ .../skill-tests/content.test.ts | 23 ++ tests/helpers/apex-validator.ts | 27 ++ tests/helpers/extract-code-blocks.ts | 42 +++ tests/helpers/index.ts | 18 ++ tests/helpers/link-checker.ts | 54 ++++ tests/helpers/parse-skill.ts | 65 +++++ tests/helpers/xml-validator.ts | 54 ++++ .../script-tests/test_analyze_trigger.py | 46 ++++ .../skill-tests/content.test.ts | 29 +++ .../script-tests/graphql_search.bats | 20 ++ .../script-tests/test_graphql_search.sh | 41 +++ .../skill-tests/content.test.ts | 27 ++ 15 files changed, 733 insertions(+) create mode 100644 tests/README.md create mode 100644 tests/generating-apex/script-tests/references.test.js create mode 100644 tests/generating-apex/script-tests/templates.test.ts create mode 100644 tests/generating-apex/skill-tests/content.test.ts create mode 100644 tests/helpers/apex-validator.ts create mode 100644 tests/helpers/extract-code-blocks.ts create mode 100644 tests/helpers/index.ts create mode 100644 tests/helpers/link-checker.ts create mode 100644 tests/helpers/parse-skill.ts create mode 100644 tests/helpers/xml-validator.ts create mode 100644 tests/trigger-refactor-pipeline/script-tests/test_analyze_trigger.py create mode 100644 tests/trigger-refactor-pipeline/skill-tests/content.test.ts create mode 100644 tests/using-ui-bundle-salesforce-data/script-tests/graphql_search.bats create mode 100644 tests/using-ui-bundle-salesforce-data/script-tests/test_graphql_search.sh create mode 100644 tests/using-ui-bundle-salesforce-data/skill-tests/content.test.ts diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..110276b --- /dev/null +++ b/tests/README.md @@ -0,0 +1,239 @@ +# Testing Guide for `afv-library` + +This directory contains all tests for the skills in this repo. Tests live here - never inside `skills/` - so the published package stays clean. + +## Quick start + +```bash +npm ci +pip install pytest # only if you have Python script-tests + +npm run test # run everything (validator + skill-tests + script-tests) + +npm run validate:skills # structural validator only +npm run test:skills # skill-tests only (Vitest) +npm run test:scripts # script-tests only (pytest, bash, bats, TypeScript/JS) +``` + +## Directory structure + +When skill owners add tests, the directory grows like this: + +``` +tests/ +├── README.md ← you are here +├── helpers/ ← shared utilities (part of the framework) +│ ├── index.ts +│ ├── parse-skill.ts +│ ├── extract-code-blocks.ts +│ ├── apex-validator.ts +│ ├── xml-validator.ts +│ └── link-checker.ts +│ +├── generating-apex/ ← one skill, one directory +│ └── skill-tests/ +│ └── content.test.ts +│ +├── testing-agentforce/ ← skill with scripts +│ ├── skill-tests/ +│ │ └── content.test.ts +│ └── script-tests/ +│ └── test_run_specs.sh +│ +└── ... +``` + +### How to organize + +The directory name doesn't have to match a skill name - Vitest and the discovery script don't check it. You can organize however makes sense for your team: + +- **One directory per skill** (e.g. `tests/generating-apex/`) - good default when one team owns one skill. +- **One directory per team** (e.g. `tests/agentforce/`) - useful when a team owns multiple skills and wants to cover them in fewer test files. + +Inside each directory, up to two folders: + + +| Folder | Purpose | Language | Runner | +| --------------- | --------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | ----------------------------- | +| `skill-tests/` | Validate SKILL.md content - frontmatter values, code block correctness, asset existence | TypeScript only | Vitest | +| `script-tests/` | Test executable scripts in their native language | Same language as your script (Python, Bash, Bats, TypeScript/JS) | pytest, bash, bats, or vitest | + + +Most skills only need `skill-tests/`. Add `script-tests/` only if your skill ships scripts in `scripts/`. + +## Adding tests for your skill + +### Step 1: Create the directory + +```bash +mkdir -p tests//skill-tests +``` + +Use the skill name (e.g. `generating-apex`) or a team name (e.g. `agentforce`) - whichever fits your ownership model. + +### Step 2: Write a skill-test + +Create a `*.test.ts` file inside `skill-tests/`. The `SKILL` constant controls which skill directory gets read - it's independent of the test directory name. + +**Single-skill example** (`tests/generating-apex/skill-tests/content.test.ts`): + +```typescript +import { describe, it, expect } from "vitest" +import { readSkillFile, skillHasFile, extractCodeBlocks, hasBalancedBraces } from "../../helpers" + +const SKILL = "generating-apex" + +describe(`${SKILL}: SKILL.md content`, () => { + const content = readSkillFile(SKILL, "SKILL.md") + + it("description mentions the key activation context", () => { + expect(content).toMatch(/Apex/) + }) +}) + +describe(`${SKILL}: code examples`, () => { + const body = readSkillFile(SKILL, "SKILL.md") + const blocks = extractCodeBlocks(body, "apex") + + it("has code blocks", () => { + expect(blocks.length).toBeGreaterThan(0) + }) + + for (const block of blocks) { + it(`block at line ${block.startLine} has balanced braces`, () => { + const result = hasBalancedBraces(block.content) + expect(result.balanced, `open=${result.open} close=${result.close}`).toBe(true) + }) + } +}) + +describe(`${SKILL}: required assets exist`, () => { + const files = ["assets/template.cls", "references/patterns.md"] + + for (const file of files) { + it(`${file} exists`, () => { + expect(skillHasFile(SKILL, file)).toBe(true) + }) + } +}) +``` + +**Multi-skill example** (`tests/agentforce/skill-tests/content.test.ts`) - one file covering multiple skills owned by the same team: + +```typescript +import { describe, it, expect } from "vitest" +import { readSkillFile, extractCodeBlocks, hasBalancedBraces } from "../../helpers" + +const SKILLS = [ + "developing-agentforce", + "testing-agentforce", + "observing-agentforce", +] + +for (const skill of SKILLS) { + describe(`${skill}: SKILL.md content`, () => { + const content = readSkillFile(skill, "SKILL.md") + + it("description mentions Agentforce", () => { + expect(content).toMatch(/Agentforce/) + }) + }) + + describe(`${skill}: code examples have balanced braces`, () => { + const body = readSkillFile(skill, "SKILL.md") + const blocks = extractCodeBlocks(body, "yaml") + + for (const block of blocks) { + it(`block at line ${block.startLine}`, () => { + const result = hasBalancedBraces(block.content) + expect(result.balanced).toBe(true) + }) + } + }) +} +``` + +Vitest discovers any `*.test.ts` file under `tests/*/skill-tests/` automatically - no registration needed. + +### Step 3: (Optional) Add script-tests + +Only needed if your skill has scripts in `skills//scripts/`. + +```bash +mkdir -p tests//script-tests +``` + +Add a test file matching the **required** naming convention for your language. Files that don't match these patterns will not be discovered and will silently not run: + + +| Language | File naming pattern | Runner | +| ------------- | -------------------------- | ------ | +| Python | `test_*.py` or `*_test.py` | pytest | +| Bash | `test_*.sh` or `*_test.sh` | bash | +| Bats | `*.bats` | bats | +| TypeScript/JS | `*.test.ts` or `*.test.js` | vitest | + + +### Step 4: Verify + +```bash +npm run test:skills # should pick up your new skill-test +npm run test:scripts # should pick up your new script-test +``` + +## Shared helpers + +The `tests/helpers/` directory provides reusable utilities so skill owners don't have to rewrite common operations. Import them in any skill-test: + +```typescript +import { + readSkillFile, + skillHasFile, + parseSkill, + extractCodeBlocks, + hasBalancedBraces, + hasClassOrInterfaceDeclaration, + containsAnnotation, + isWellFormedXml, + findBrokenLinks, +} from "../../helpers" +``` + +Browse `tests/helpers/index.ts` to see all available exports. The helpers cover skill file access, code block extraction, Apex structural checks, XML validation, and link checking. + +Need a helper that doesn't exist yet? Add it to `tests/helpers/` and export it from `index.ts`. + +## How discovery works + +**Skill-tests**: Vitest finds all `*.test.{ts,js}` files (configured in `vitest.config.ts`). The `npm run test:skills` command filters to only files with `skill-tests` in the path. No registration needed - drop the file and it runs. + +**Script-tests**: The discovery script (`scripts/run-skill-tests.sh`) walks every `tests/*/script-tests/` directory, matches test files by extension, and dispatches to the native runner. If a required runner isn't installed (e.g. pytest), it prints `SKIPPED` instead of failing. + +The discovery script matches files to runners as follows: + + +| Language | Finds files matching | Runs them with | +| ------------- | ------------------------ | -------------- | +| Python | `test_*.py`, `*_test.py` | pytest | +| Bash | `test_*.sh`, `*_test.sh` | bash | +| Bats | `*.bats` | bats | +| TypeScript/JS | `*.test.ts`, `*.test.js` | vitest | + + +**Naming matters.** If your test file doesn't match the expected pattern, it will silently not run: + + +| Runs | Does NOT run | +| ------------------ | --------------------------------------------- | +| `test_analyzer.py` | `analyzer_tests.py` | +| `analyzer_test.py` | `test-analyzer.py` (hyphens, not underscores) | +| `test_search.sh` | `search_tests.sh` | +| `content.test.ts` | `content.spec.ts` | + + +## Prerequisites + +- **Node.js** (v22, see `.nvmrc`) + `npm ci` +- **Python 3.9+** and `pip install pytest` - only needed if you have Python script-tests +- No additional tools needed for bash script-tests + diff --git a/tests/generating-apex/script-tests/references.test.js b/tests/generating-apex/script-tests/references.test.js new file mode 100644 index 0000000..0b5ae50 --- /dev/null +++ b/tests/generating-apex/script-tests/references.test.js @@ -0,0 +1,22 @@ +import { describe, it, expect } from "vitest" +import fs from "fs" +import path from "path" + +const REFS_DIR = path.join(__dirname, "..", "..", "..", "skills", "generating-apex", "references") + +describe("generating-apex: reference files are valid", () => { + it("references directory exists and has .cls files", () => { + const files = fs.readdirSync(REFS_DIR).filter((f) => f.endsWith(".cls")) + expect(files.length).toBeGreaterThan(0) + }) + + it("all reference files are non-empty", () => { + const files = fs.readdirSync(REFS_DIR).filter((f) => f.endsWith(".cls")) + for (const file of files) { + const content = fs.readFileSync(path.join(REFS_DIR, file), "utf8") + expect(content.trim().length, `${file} should not be empty`).toBeGreaterThan(0) + } + }) + + // TODO: Add more tests - validate class names match filenames, check for ApexDoc, etc. +}) diff --git a/tests/generating-apex/script-tests/templates.test.ts b/tests/generating-apex/script-tests/templates.test.ts new file mode 100644 index 0000000..a0495ca --- /dev/null +++ b/tests/generating-apex/script-tests/templates.test.ts @@ -0,0 +1,26 @@ +import { describe, it, expect } from "vitest" +import fs from "fs" +import path from "path" + +const TEMPLATES_DIR = path.join(__dirname, "..", "..", "..", "skills", "generating-apex", "assets") + +describe("generating-apex: template files are valid", () => { + it("all .cls files are non-empty", () => { + const files = fs.readdirSync(TEMPLATES_DIR).filter((f) => f.endsWith(".cls")) + expect(files.length).toBeGreaterThan(0) + for (const file of files) { + const content = fs.readFileSync(path.join(TEMPLATES_DIR, file), "utf8") + expect(content.trim().length, `${file} should not be empty`).toBeGreaterThan(0) + } + }) + + it("every template has a placeholder token", () => { + const files = fs.readdirSync(TEMPLATES_DIR).filter((f) => f.endsWith(".cls")) + for (const file of files) { + const content = fs.readFileSync(path.join(TEMPLATES_DIR, file), "utf8") + expect(content, `${file} should have a {placeholder}`).toMatch(/\{[A-Z]/) + } + }) + + // TODO: Add more tests - validate specific templates, check for required methods, etc. +}) diff --git a/tests/generating-apex/skill-tests/content.test.ts b/tests/generating-apex/skill-tests/content.test.ts new file mode 100644 index 0000000..0b86e43 --- /dev/null +++ b/tests/generating-apex/skill-tests/content.test.ts @@ -0,0 +1,23 @@ +import { describe, it, expect } from "vitest" +import { readSkillFile, skillHasFile, extractCodeBlocks, hasBalancedBraces } from "../../helpers" + +const SKILL = "generating-apex" + +describe(`${SKILL}: Apex template assets`, () => { + it("service.cls template exists", () => { + expect(skillHasFile(SKILL, "assets/service.cls")).toBe(true) + }) + + it("batch.cls template has balanced braces", () => { + const content = readSkillFile(SKILL, "assets/batch.cls") + const result = hasBalancedBraces(content) + expect(result.balanced, `open=${result.open} close=${result.close}`).toBe(true) + }) + + it("batch.cls implements Database.Batchable", () => { + const content = readSkillFile(SKILL, "assets/batch.cls") + expect(content).toContain("Database.Batchable") + }) + + // TODO: Add more tests - validate remaining templates, check code blocks in SKILL.md, etc. +}) diff --git a/tests/helpers/apex-validator.ts b/tests/helpers/apex-validator.ts new file mode 100644 index 0000000..ea5c817 --- /dev/null +++ b/tests/helpers/apex-validator.ts @@ -0,0 +1,27 @@ +/** + * Lightweight structural checks for Apex .cls files. + * These do NOT compile Apex — they verify basic structural integrity + * via string matching. Sufficient for catching template corruption. + */ + +export function hasClassOrInterfaceDeclaration(content: string): boolean { + return /\b(class|interface)\s+[\w{}\[\]]+/.test(content) +} + +export function hasBalancedBraces(content: string): { balanced: boolean; open: number; close: number } { + const open = (content.match(/{/g) || []).length + const close = (content.match(/}/g) || []).length + return { balanced: open === close, open, close } +} + +export function containsAnnotation(content: string, annotation: string): boolean { + return content.includes(annotation) +} + +export function containsKeyword(content: string, keyword: string): boolean { + return new RegExp(`\\b${keyword}\\b`).test(content) +} + +export function hasApexDoc(content: string): boolean { + return content.includes("@param") || content.includes("@return") || content.includes("@description") +} diff --git a/tests/helpers/extract-code-blocks.ts b/tests/helpers/extract-code-blocks.ts new file mode 100644 index 0000000..771eca0 --- /dev/null +++ b/tests/helpers/extract-code-blocks.ts @@ -0,0 +1,42 @@ +export interface CodeBlock { + language: string + content: string + startLine: number +} + +/** + * Extracts fenced code blocks from markdown content. + * Optionally filters by language tag. + */ +export function extractCodeBlocks(markdown: string, language?: string): CodeBlock[] { + const blocks: CodeBlock[] = [] + const lines = markdown.split("\n") + let inBlock = false + let currentLang = "" + let currentContent: string[] = [] + let blockStart = 0 + + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + + if (!inBlock && line.match(/^```(\w*)/)) { + inBlock = true + currentLang = line.match(/^```(\w*)/)![1] || "" + currentContent = [] + blockStart = i + 1 + } else if (inBlock && line.trim() === "```") { + inBlock = false + if (!language || currentLang.toLowerCase() === language.toLowerCase()) { + blocks.push({ + language: currentLang, + content: currentContent.join("\n"), + startLine: blockStart, + }) + } + } else if (inBlock) { + currentContent.push(line) + } + } + + return blocks +} diff --git a/tests/helpers/index.ts b/tests/helpers/index.ts new file mode 100644 index 0000000..d6dfe57 --- /dev/null +++ b/tests/helpers/index.ts @@ -0,0 +1,18 @@ +export { parseSkill, listSkillDirs, getSkillsDir, skillHasFile, readSkillFile } from "./parse-skill" +export type { ParsedSkill } from "./parse-skill" + +export { extractCodeBlocks } from "./extract-code-blocks" +export type { CodeBlock } from "./extract-code-blocks" + +export { isWellFormedXml } from "./xml-validator" + +export { + hasClassOrInterfaceDeclaration, + hasBalancedBraces, + containsAnnotation, + containsKeyword, + hasApexDoc, +} from "./apex-validator" + +export { extractRelativeLinks, findBrokenLinks } from "./link-checker" +export type { BrokenLink } from "./link-checker" diff --git a/tests/helpers/link-checker.ts b/tests/helpers/link-checker.ts new file mode 100644 index 0000000..140601d --- /dev/null +++ b/tests/helpers/link-checker.ts @@ -0,0 +1,54 @@ +import fs from "fs" +import path from "path" + +export interface BrokenLink { + link: string + text: string + reason: string +} + +/** + * Extracts relative markdown links from content. + * Ignores URLs (http/https), anchors-only (#heading), and mailto links. + */ +export function extractRelativeLinks(content: string): Array<{ text: string; link: string }> { + const linkRegex = /\[([^\]]*)\]\(([^)]+)\)/g + const links: Array<{ text: string; link: string }> = [] + let match: RegExpExecArray | null + + while ((match = linkRegex.exec(content)) !== null) { + const link = match[2] + if (link.startsWith("http://") || link.startsWith("https://") || link.startsWith("#") || link.startsWith("mailto:")) { + continue + } + const filePath = link.split("#")[0] + if (filePath) { + links.push({ text: match[1], link: filePath }) + } + } + + return links +} + +/** + * Checks all relative markdown links in a SKILL.md resolve to existing files. + */ +export function findBrokenLinks(skillDir: string, content: string): BrokenLink[] { + const links = extractRelativeLinks(content) + const broken: BrokenLink[] = [] + + for (const { text, link } of links) { + const resolved = path.resolve(skillDir, link) + + if (!resolved.startsWith(path.resolve(skillDir))) { + broken.push({ link, text, reason: `Escapes skill directory via ../` }) + continue + } + + if (!fs.existsSync(resolved)) { + broken.push({ link, text, reason: `File not found: ${link}` }) + } + } + + return broken +} diff --git a/tests/helpers/parse-skill.ts b/tests/helpers/parse-skill.ts new file mode 100644 index 0000000..6de8a8a --- /dev/null +++ b/tests/helpers/parse-skill.ts @@ -0,0 +1,65 @@ +import fs from "fs" +import path from "path" +import yaml from "js-yaml" + +const SKILLS_DIR = path.join(__dirname, "..", "..", "skills") + +export interface ParsedSkill { + dirName: string + dirPath: string + content: string + rawFrontmatter: string | null + frontmatter: Record | null + body: string +} + +export function getSkillsDir(): string { + return SKILLS_DIR +} + +export function listSkillDirs(): string[] { + return fs.readdirSync(SKILLS_DIR).filter((entry) => { + const fullPath = path.join(SKILLS_DIR, entry) + return fs.statSync(fullPath).isDirectory() && !entry.startsWith(".") + }) +} + +export function parseSkill(dirName: string): ParsedSkill { + const dirPath = path.join(SKILLS_DIR, dirName) + const skillMdPath = path.join(dirPath, "SKILL.md") + + if (!fs.existsSync(skillMdPath)) { + return { dirName, dirPath, content: "", rawFrontmatter: null, frontmatter: null, body: "" } + } + + const content = fs.readFileSync(skillMdPath, "utf8") + const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/) + + if (!match) { + return { dirName, dirPath, content, rawFrontmatter: null, frontmatter: null, body: content } + } + + const rawFrontmatter = match[1] + let frontmatter: Record | null = null + + try { + const parsed = yaml.load(rawFrontmatter) + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + frontmatter = parsed as Record + } + } catch { + frontmatter = null + } + + const body = content.slice(match[0].length) + + return { dirName, dirPath, content, rawFrontmatter, frontmatter, body } +} + +export function skillHasFile(dirName: string, relativePath: string): boolean { + return fs.existsSync(path.join(SKILLS_DIR, dirName, relativePath)) +} + +export function readSkillFile(dirName: string, relativePath: string): string { + return fs.readFileSync(path.join(SKILLS_DIR, dirName, relativePath), "utf8") +} diff --git a/tests/helpers/xml-validator.ts b/tests/helpers/xml-validator.ts new file mode 100644 index 0000000..b9b18a1 --- /dev/null +++ b/tests/helpers/xml-validator.ts @@ -0,0 +1,54 @@ +/** + * Lightweight XML well-formedness check. + * Verifies the string looks like valid XML by checking: + * - Has an opening tag + * - All opened tags are closed (or self-closing) + * - No obvious structural errors + * + * For a full XML parse, use DOMParser or a library. + * This is sufficient for catching common template corruption. + */ +export function isWellFormedXml(xml: string): { valid: boolean; error?: string } { + const trimmed = xml.trim() + + if (!trimmed.startsWith("<")) { + return { valid: false, error: "Does not start with <" } + } + + const stripped = trimmed.replace(//g, "") + + const tagStack: string[] = [] + const tagRegex = /<\/?([a-zA-Z_][\w:.-]*)[^>]*?\/?>/g + let match: RegExpExecArray | null + + while ((match = tagRegex.exec(stripped)) !== null) { + const fullMatch = match[0] + const tagName = match[1] + + if (fullMatch.startsWith("")) { + continue + } + + if (fullMatch.startsWith(" without matching open tag` } + } + const expected = tagStack.pop() + if (expected !== tagName) { + return { valid: false, error: `Expected closing tag , found ` } + } + } else { + tagStack.push(tagName) + } + } + + if (tagStack.length > 0) { + return { valid: false, error: `Unclosed tags: ${tagStack.join(", ")}` } + } + + return { valid: true } +} diff --git a/tests/trigger-refactor-pipeline/script-tests/test_analyze_trigger.py b/tests/trigger-refactor-pipeline/script-tests/test_analyze_trigger.py new file mode 100644 index 0000000..181d100 --- /dev/null +++ b/tests/trigger-refactor-pipeline/script-tests/test_analyze_trigger.py @@ -0,0 +1,46 @@ +import sys +import os + +sys.path.insert(0, os.path.join( + os.path.dirname(__file__), "..", "..", "..", "skills", + "trigger-refactor-pipeline", "scripts", +)) + +from analyze_trigger import TriggerAnalyzer + +CLEAN_TRIGGER = """\ +trigger AccountTrigger on Account (before insert) { + for (Account a : Trigger.new) { + if (a.Name == null) { + a.Name = 'Default'; + } + } +}""" + +DML_IN_LOOP_TRIGGER = """\ +trigger OpportunityTrigger on Opportunity (after update) { + for (Opportunity o : Trigger.new) { + Task t = new Task(WhatId = o.Id, Subject = 'Follow up'); + insert t; + } +}""" + + +class TestTriggerAnalyzer: + def test_initializes_with_name(self): + analyzer = TriggerAnalyzer("MyTrigger") + assert analyzer.trigger_name == "MyTrigger" + + def test_clean_trigger_has_no_dml_issues(self): + analyzer = TriggerAnalyzer("AccountTrigger") + analyzer.trigger_body = CLEAN_TRIGGER + analyzer.analyze_dml_in_loops() + assert len(analyzer.issues["dml_in_loops"]) == 0 + + def test_detects_dml_in_loop(self): + analyzer = TriggerAnalyzer("OpportunityTrigger") + analyzer.trigger_body = DML_IN_LOOP_TRIGGER + analyzer.analyze_dml_in_loops() + assert len(analyzer.issues["dml_in_loops"]) == 1 + + # TODO: Add more tests - SOQL in loops, bulkification, complexity scoring, etc. diff --git a/tests/trigger-refactor-pipeline/skill-tests/content.test.ts b/tests/trigger-refactor-pipeline/skill-tests/content.test.ts new file mode 100644 index 0000000..70cc2b4 --- /dev/null +++ b/tests/trigger-refactor-pipeline/skill-tests/content.test.ts @@ -0,0 +1,29 @@ +import { describe, it, expect } from "vitest" +import { readSkillFile, skillHasFile, extractCodeBlocks, hasBalancedBraces } from "../../helpers" + +const SKILL = "trigger-refactor-pipeline" + +describe(`${SKILL}: SKILL.md content`, () => { + const content = readSkillFile(SKILL, "SKILL.md") + + it("declares Python 3.9+ compatibility", () => { + expect(content).toMatch(/compatibility:.*Python 3\.9/) + }) + + it("has Apex code blocks with balanced braces", () => { + const blocks = extractCodeBlocks(content, "apex") + expect(blocks.length).toBeGreaterThan(0) + for (const block of blocks) { + const result = hasBalancedBraces(block.content) + expect(result.balanced, `block at line ${block.startLine}`).toBe(true) + } + }) + + it("required assets exist", () => { + expect(skillHasFile(SKILL, "scripts/analyze_trigger.py")).toBe(true) + expect(skillHasFile(SKILL, "assets/test_template.apex")).toBe(true) + expect(skillHasFile(SKILL, "references/handler_patterns.md")).toBe(true) + }) + + // TODO: Add more tests - validate test_template.apex structure, check bash code blocks, etc. +}) diff --git a/tests/using-ui-bundle-salesforce-data/script-tests/graphql_search.bats b/tests/using-ui-bundle-salesforce-data/script-tests/graphql_search.bats new file mode 100644 index 0000000..d87910f --- /dev/null +++ b/tests/using-ui-bundle-salesforce-data/script-tests/graphql_search.bats @@ -0,0 +1,20 @@ +#!/usr/bin/env bats + +SCRIPT="skills/using-ui-bundle-salesforce-data/scripts/graphql-search.sh" + +@test "exits 1 with no arguments" { + run bash "$SCRIPT" + [ "$status" -eq 1 ] +} + +@test "shows usage text when called with no arguments" { + run bash "$SCRIPT" + [[ "$output" == *"Usage:"* ]] +} + +@test "exits 1 when schema file does not exist" { + run bash "$SCRIPT" -s /nonexistent/schema.graphql Account + [ "$status" -eq 1 ] +} + +# TODO: Add more tests - valid schema lookup, entity extraction, multiple entities, etc. diff --git a/tests/using-ui-bundle-salesforce-data/script-tests/test_graphql_search.sh b/tests/using-ui-bundle-salesforce-data/script-tests/test_graphql_search.sh new file mode 100644 index 0000000..e0c786d --- /dev/null +++ b/tests/using-ui-bundle-salesforce-data/script-tests/test_graphql_search.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +set -euo pipefail + +PASS=0; FAIL=0 +SCRIPT="skills/using-ui-bundle-salesforce-data/scripts/graphql-search.sh" + +assert_eq() { + local label="$1" expected="$2" actual="$3" + if [[ "$expected" == "$actual" ]]; then + echo " ✓ $label"; PASS=$((PASS + 1)) + else + echo " ✗ $label (expected '$expected', got '$actual')"; FAIL=$((FAIL + 1)) + fi +} + +assert_contains() { + local label="$1" haystack="$2" needle="$3" + if [[ "$haystack" == *"$needle"* ]]; then + echo " ✓ $label"; PASS=$((PASS + 1)) + else + echo " ✗ $label (expected to contain '$needle')"; FAIL=$((FAIL + 1)) + fi +} + +echo "--- no arguments: prints usage and exits 1 ---" +rc=0; bash "$SCRIPT" >/dev/null 2>&1 || rc=$? +assert_eq "exits 1 with no args" "1" "$rc" + +echo "--- missing schema file: exits 1 ---" +rc=0; bash "$SCRIPT" -s /nonexistent/schema.graphql Account >/dev/null 2>&1 || rc=$? +assert_eq "exits 1 on missing schema" "1" "$rc" + +echo "--- no args shows usage text ---" +output=$(bash "$SCRIPT" 2>&1 || true) +assert_contains "shows usage" "$output" "Usage:" + +# TODO: Add more tests - valid schema lookup, multiple entities, unknown entity warning, etc. + +echo "" +echo "$PASS passed, $FAIL failed" +[[ $FAIL -eq 0 ]] diff --git a/tests/using-ui-bundle-salesforce-data/skill-tests/content.test.ts b/tests/using-ui-bundle-salesforce-data/skill-tests/content.test.ts new file mode 100644 index 0000000..cb96ed9 --- /dev/null +++ b/tests/using-ui-bundle-salesforce-data/skill-tests/content.test.ts @@ -0,0 +1,27 @@ +import { describe, it, expect } from "vitest" +import { readSkillFile, skillHasFile, extractCodeBlocks, hasBalancedBraces } from "../../helpers" + +const SKILL = "using-ui-bundle-salesforce-data" + +describe(`${SKILL}: SKILL.md content`, () => { + const content = readSkillFile(SKILL, "SKILL.md") + + it("description mentions Salesforce record operations", () => { + expect(content).toMatch(/Salesforce record operation/) + }) + + it("has GraphQL code blocks with balanced braces", () => { + const blocks = extractCodeBlocks(content, "graphql") + expect(blocks.length).toBeGreaterThan(0) + for (const block of blocks) { + const result = hasBalancedBraces(block.content) + expect(result.balanced, `block at line ${block.startLine}`).toBe(true) + } + }) + + it("required assets exist", () => { + expect(skillHasFile(SKILL, "scripts/graphql-search.sh")).toBe(true) + }) + + // TODO: Add more tests - validate TypeScript code blocks, check non-negotiable rules, etc. +})