all-hands-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.allhands/README.md +75 -0
- package/.allhands/agents/compounder.yaml +15 -0
- package/.allhands/agents/coordinator.yaml +17 -0
- package/.allhands/agents/documentor.yaml +15 -0
- package/.allhands/agents/e2e-test-planner.yaml +17 -0
- package/.allhands/agents/emergent.yaml +22 -0
- package/.allhands/agents/executor.yaml +14 -0
- package/.allhands/agents/ideation.yaml +11 -0
- package/.allhands/agents/initiative-steering.yaml +19 -0
- package/.allhands/agents/judge.yaml +13 -0
- package/.allhands/agents/planner.yaml +19 -0
- package/.allhands/agents/pr-reviewer.yaml +15 -0
- package/.allhands/docs.json +5 -0
- package/.allhands/docs.local.json +26 -0
- package/.allhands/flows/COMPOUNDING.md +203 -0
- package/.allhands/flows/COORDINATION.md +89 -0
- package/.allhands/flows/CORE.md +87 -0
- package/.allhands/flows/DOCUMENTATION.md +218 -0
- package/.allhands/flows/E2E_TEST_PLAN_BUILDING.md +140 -0
- package/.allhands/flows/EMERGENT_PLANNING.md +57 -0
- package/.allhands/flows/IDEATION_SCOPING.md +154 -0
- package/.allhands/flows/INITIATIVE_STEERING.md +110 -0
- package/.allhands/flows/JUDGE_REVIEWING.md +79 -0
- package/.allhands/flows/PROMPT_TASK_EXECUTION.md +68 -0
- package/.allhands/flows/PR_REVIEWING.md +43 -0
- package/.allhands/flows/SPEC_PLANNING.md +216 -0
- package/.allhands/flows/harness/WRITING_HARNESS_FLOWS.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_KNOWLEDGE.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_ORCHESTRATION.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_SKILLS.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_TOOLS.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_VALIDATION_TOOLING.md +27 -0
- package/.allhands/flows/shared/CODEBASE_UNDERSTANDING.md +72 -0
- package/.allhands/flows/shared/CREATE_HARNESS_SPEC.md +48 -0
- package/.allhands/flows/shared/CREATE_SPEC.md +41 -0
- package/.allhands/flows/shared/CREATE_VALIDATION_TOOLING_SPEC.md +70 -0
- package/.allhands/flows/shared/DOCUMENTATION_DISCOVERY.md +123 -0
- package/.allhands/flows/shared/DOCUMENTATION_WRITER.md +101 -0
- package/.allhands/flows/shared/EMERGENT_REFINEMENT_ANALYSIS.md +76 -0
- package/.allhands/flows/shared/EXTERNAL_TECH_GUIDANCE.md +97 -0
- package/.allhands/flows/shared/IDEATION_CODEBASE_GROUNDING.md +49 -0
- package/.allhands/flows/shared/PLAN_DEEPENING.md +152 -0
- package/.allhands/flows/shared/PROMPT_TASKS_CURATION.md +113 -0
- package/.allhands/flows/shared/PROMPT_VALIDATION_REVIEW.MD +99 -0
- package/.allhands/flows/shared/QUICK_PREMORTEM.md +70 -0
- package/.allhands/flows/shared/RESEARCH_GUIDANCE.md +38 -0
- package/.allhands/flows/shared/REVIEW_OPTIONS_BREAKDOWN.md +68 -0
- package/.allhands/flows/shared/SKILL_EXTRACTION.md +84 -0
- package/.allhands/flows/shared/SPEC_FLOW_ANALYSIS.md +119 -0
- package/.allhands/flows/shared/TDD_WORKFLOW.md +109 -0
- package/.allhands/flows/shared/UTILIZE_VALIDATION_TOOLING.md +84 -0
- package/.allhands/flows/shared/WRITING_HARNESS_FLOWS.md +11 -0
- package/.allhands/flows/shared/WRITING_HARNESS_MCP_TOOLS.md +84 -0
- package/.allhands/flows/shared/jury/ARCHITECTURE_REVIEW.md +91 -0
- package/.allhands/flows/shared/jury/BEST_PRACTICES_REVIEW.md +80 -0
- package/.allhands/flows/shared/jury/CLAIM_VERIFICATION_REVIEW.md +101 -0
- package/.allhands/flows/shared/jury/EXPECTATIONS_FIT_REVIEW.md +78 -0
- package/.allhands/flows/shared/jury/MAINTAINABILITY_REVIEW.md +110 -0
- package/.allhands/flows/shared/jury/PROMPTS_EXPECTATIONS_FIT.md +74 -0
- package/.allhands/flows/shared/jury/PROMPTS_FLOW_ANALYSIS.md +92 -0
- package/.allhands/flows/shared/jury/PROMPTS_YAGNI.md +78 -0
- package/.allhands/flows/shared/jury/PROMPT_PREMORTEM.md +125 -0
- package/.allhands/flows/shared/jury/SECURITY_REVIEW.md +86 -0
- package/.allhands/flows/shared/jury/YAGNI_REVIEW.md +82 -0
- package/.allhands/flows/wip/DEBUG_INVESTIGATION.md +162 -0
- package/.allhands/flows/wip/MEMORY_RECALL.md +62 -0
- package/.allhands/harness/ah +131 -0
- package/.allhands/harness/package-lock.json +5292 -0
- package/.allhands/harness/package.json +52 -0
- package/.allhands/harness/src/__tests__/e2e/commands.test.ts +307 -0
- package/.allhands/harness/src/__tests__/e2e/event-loop.test.ts +539 -0
- package/.allhands/harness/src/__tests__/e2e/hooks.test.ts +427 -0
- package/.allhands/harness/src/__tests__/e2e/new-initiative-routing.test.ts +137 -0
- package/.allhands/harness/src/__tests__/e2e/run-e2e.ts +109 -0
- package/.allhands/harness/src/__tests__/e2e/specs-type.test.ts +210 -0
- package/.allhands/harness/src/__tests__/e2e/validation-hooks.test.ts +669 -0
- package/.allhands/harness/src/__tests__/e2e/validation-path-consistency.test.ts +354 -0
- package/.allhands/harness/src/__tests__/e2e/validation.test.ts +528 -0
- package/.allhands/harness/src/__tests__/harness/assertions.ts +318 -0
- package/.allhands/harness/src/__tests__/harness/cli-runner.ts +359 -0
- package/.allhands/harness/src/__tests__/harness/fixture.ts +384 -0
- package/.allhands/harness/src/__tests__/harness/hook-runner.ts +411 -0
- package/.allhands/harness/src/__tests__/harness/index.ts +122 -0
- package/.allhands/harness/src/cli.ts +36 -0
- package/.allhands/harness/src/commands/complexity.ts +177 -0
- package/.allhands/harness/src/commands/context7.ts +202 -0
- package/.allhands/harness/src/commands/docs.ts +557 -0
- package/.allhands/harness/src/commands/hooks.ts +24 -0
- package/.allhands/harness/src/commands/index.ts +51 -0
- package/.allhands/harness/src/commands/knowledge.ts +382 -0
- package/.allhands/harness/src/commands/memories.ts +302 -0
- package/.allhands/harness/src/commands/notify.ts +61 -0
- package/.allhands/harness/src/commands/oracle.ts +158 -0
- package/.allhands/harness/src/commands/perplexity.ts +220 -0
- package/.allhands/harness/src/commands/planning.ts +245 -0
- package/.allhands/harness/src/commands/schema.ts +73 -0
- package/.allhands/harness/src/commands/skills.ts +128 -0
- package/.allhands/harness/src/commands/solutions.ts +353 -0
- package/.allhands/harness/src/commands/spawn.ts +158 -0
- package/.allhands/harness/src/commands/specs.ts +532 -0
- package/.allhands/harness/src/commands/tavily.ts +226 -0
- package/.allhands/harness/src/commands/tools.ts +579 -0
- package/.allhands/harness/src/commands/trace.ts +327 -0
- package/.allhands/harness/src/commands/tui.ts +960 -0
- package/.allhands/harness/src/commands/validate.ts +143 -0
- package/.allhands/harness/src/commands/validation-tools.ts +108 -0
- package/.allhands/harness/src/hooks/context.ts +1442 -0
- package/.allhands/harness/src/hooks/enforcement.ts +170 -0
- package/.allhands/harness/src/hooks/index.ts +54 -0
- package/.allhands/harness/src/hooks/lifecycle.ts +229 -0
- package/.allhands/harness/src/hooks/notification.ts +104 -0
- package/.allhands/harness/src/hooks/observability.ts +551 -0
- package/.allhands/harness/src/hooks/session.ts +88 -0
- package/.allhands/harness/src/hooks/shared.ts +815 -0
- package/.allhands/harness/src/hooks/transcript-parser.ts +208 -0
- package/.allhands/harness/src/hooks/validation.ts +617 -0
- package/.allhands/harness/src/lib/__tests__/ctags.test.ts +244 -0
- package/.allhands/harness/src/lib/__tests__/docs-validation.test.ts +344 -0
- package/.allhands/harness/src/lib/__tests__/mcp-runtime.test.ts +190 -0
- package/.allhands/harness/src/lib/__tests__/schema.test.ts +861 -0
- package/.allhands/harness/src/lib/base-command.ts +198 -0
- package/.allhands/harness/src/lib/cli-daemon.ts +343 -0
- package/.allhands/harness/src/lib/compaction.ts +313 -0
- package/.allhands/harness/src/lib/ctags.ts +497 -0
- package/.allhands/harness/src/lib/docs-validation.ts +907 -0
- package/.allhands/harness/src/lib/event-loop.ts +662 -0
- package/.allhands/harness/src/lib/flows.ts +155 -0
- package/.allhands/harness/src/lib/git.ts +276 -0
- package/.allhands/harness/src/lib/knowledge-worker.ts +72 -0
- package/.allhands/harness/src/lib/knowledge.ts +810 -0
- package/.allhands/harness/src/lib/llm.ts +255 -0
- package/.allhands/harness/src/lib/mcp-client.ts +432 -0
- package/.allhands/harness/src/lib/mcp-daemon.ts +486 -0
- package/.allhands/harness/src/lib/mcp-runtime.ts +418 -0
- package/.allhands/harness/src/lib/notification.ts +115 -0
- package/.allhands/harness/src/lib/opencode/index.ts +70 -0
- package/.allhands/harness/src/lib/opencode/profiles.ts +300 -0
- package/.allhands/harness/src/lib/opencode/prompts/codesearch.md +98 -0
- package/.allhands/harness/src/lib/opencode/prompts/knowledge-aggregator.md +67 -0
- package/.allhands/harness/src/lib/opencode/runner.ts +281 -0
- package/.allhands/harness/src/lib/oracle.ts +926 -0
- package/.allhands/harness/src/lib/planning-utils.ts +150 -0
- package/.allhands/harness/src/lib/planning.ts +605 -0
- package/.allhands/harness/src/lib/pr-review.ts +225 -0
- package/.allhands/harness/src/lib/prompts.ts +522 -0
- package/.allhands/harness/src/lib/schema.ts +418 -0
- package/.allhands/harness/src/lib/schemas/agent-profile.ts +141 -0
- package/.allhands/harness/src/lib/schemas/template-vars.ts +138 -0
- package/.allhands/harness/src/lib/session.ts +164 -0
- package/.allhands/harness/src/lib/specs.ts +348 -0
- package/.allhands/harness/src/lib/tldr.ts +829 -0
- package/.allhands/harness/src/lib/tmux.ts +1051 -0
- package/.allhands/harness/src/lib/trace-store.ts +714 -0
- package/.allhands/harness/src/mcp/__tests__/index.test.ts +46 -0
- package/.allhands/harness/src/mcp/_template.ts +47 -0
- package/.allhands/harness/src/mcp/filesystem.ts +33 -0
- package/.allhands/harness/src/mcp/index.ts +69 -0
- package/.allhands/harness/src/mcp/playwright.ts +34 -0
- package/.allhands/harness/src/mcp/xcodebuild.ts +29 -0
- package/.allhands/harness/src/schemas/docs.schema.json +44 -0
- package/.allhands/harness/src/schemas/settings.schema.json +214 -0
- package/.allhands/harness/src/tui/actions.ts +227 -0
- package/.allhands/harness/src/tui/file-viewer-modal.ts +270 -0
- package/.allhands/harness/src/tui/index.ts +1574 -0
- package/.allhands/harness/src/tui/modal.ts +232 -0
- package/.allhands/harness/src/tui/prompts-pane.ts +186 -0
- package/.allhands/harness/src/tui/status-pane.ts +434 -0
- package/.allhands/harness/tsconfig.json +22 -0
- package/.allhands/harness/vitest.config.ts +13 -0
- package/.allhands/pillars.md +33 -0
- package/.allhands/principles.md +88 -0
- package/.allhands/schemas/alignment.yaml +51 -0
- package/.allhands/schemas/documentation.yaml +10 -0
- package/.allhands/schemas/prompt.yaml +92 -0
- package/.allhands/schemas/skill.yaml +34 -0
- package/.allhands/schemas/solution.yaml +131 -0
- package/.allhands/schemas/spec.yaml +67 -0
- package/.allhands/schemas/validation-suite.yaml +49 -0
- package/.allhands/schemas/workflow.yaml +51 -0
- package/.allhands/settings.json +57 -0
- package/.allhands/skills/claude-code-patterns/SKILL.md +60 -0
- package/.allhands/skills/claude-code-patterns/docs/context-hygiene.md +19 -0
- package/.allhands/skills/harness-maintenance/SKILL.md +449 -0
- package/.allhands/skills/harness-maintenance/references/core-architecture.md +187 -0
- package/.allhands/skills/harness-maintenance/references/harness-skills.md +87 -0
- package/.allhands/skills/harness-maintenance/references/knowledge-compounding.md +78 -0
- package/.allhands/skills/harness-maintenance/references/tools-commands-mcp-hooks.md +115 -0
- package/.allhands/skills/harness-maintenance/references/validation-tooling.md +77 -0
- package/.allhands/skills/harness-maintenance/references/writing-flows.md +84 -0
- package/.allhands/validation/browser-automation.md +109 -0
- package/.allhands/validation/xcode-automation.md +195 -0
- package/.allhands/workflows/documentation.md +86 -0
- package/.allhands/workflows/investigation.md +81 -0
- package/.allhands/workflows/milestone.md +91 -0
- package/.allhands/workflows/optimization.md +85 -0
- package/.allhands/workflows/refactor.md +99 -0
- package/.allhands/workflows/triage.md +81 -0
- package/.claude/README.md +1 -0
- package/.claude/agents/explorer.md +10 -0
- package/.claude/agents/researcher.md +11 -0
- package/.claude/agents/task-runner.md +8 -0
- package/.claude/settings.json +231 -0
- package/.env.ai.example +7 -0
- package/.github/workflows/npm-publish.yml +69 -0
- package/.internal.json +45 -0
- package/.tldr/config.json +11 -0
- package/.tldrignore +90 -0
- package/CLAUDE.md +6 -0
- package/README.md +98 -0
- package/bin/sync-cli.js +7552 -0
- package/concerns.md +7 -0
- package/docs/README.md +41 -0
- package/docs/agents/README.md +24 -0
- package/docs/agents/agent-configuration-system.md +86 -0
- package/docs/agents/execution-agents.md +50 -0
- package/docs/agents/knowledge-agents.md +61 -0
- package/docs/agents/orchestration-agent.md +57 -0
- package/docs/agents/planning-agents.md +84 -0
- package/docs/agents/quality-review-agents.md +67 -0
- package/docs/agents/workflow-agent-orchestration.md +69 -0
- package/docs/flows/README.md +44 -0
- package/docs/flows/compounding.md +126 -0
- package/docs/flows/coordination.md +72 -0
- package/docs/flows/core-harness-integration.md +63 -0
- package/docs/flows/documentation-orchestration.md +98 -0
- package/docs/flows/e2e-test-plan-building.md +83 -0
- package/docs/flows/emergent-refinement.md +104 -0
- package/docs/flows/flow-authoring-and-mcp-tools.md +89 -0
- package/docs/flows/judge-reviewing.md +112 -0
- package/docs/flows/plan-deepening-and-research.md +107 -0
- package/docs/flows/plan-review-jury.md +114 -0
- package/docs/flows/pr-reviewing.md +54 -0
- package/docs/flows/prompt-task-execution.md +119 -0
- package/docs/flows/spec-planning.md +162 -0
- package/docs/flows/type-specific-scoping-flows.md +49 -0
- package/docs/flows/validation-and-skills-integration.md +145 -0
- package/docs/flows/wip/wip-flows.md +102 -0
- package/docs/harness/README.md +23 -0
- package/docs/harness/agent-profiles.md +84 -0
- package/docs/harness/cli/README.md +24 -0
- package/docs/harness/cli/cli-entry-and-command-discovery.md +91 -0
- package/docs/harness/cli/docs-command.md +87 -0
- package/docs/harness/cli/knowledge-command.md +91 -0
- package/docs/harness/cli/minor-cli-commands.md +65 -0
- package/docs/harness/cli/oracle-command.md +113 -0
- package/docs/harness/cli/planning-command.md +95 -0
- package/docs/harness/cli/schema-and-validation-commands.md +154 -0
- package/docs/harness/cli/search-commands.md +97 -0
- package/docs/harness/cli/spawn-command.md +136 -0
- package/docs/harness/cli/specs-command.md +102 -0
- package/docs/harness/cli/tools-command.md +122 -0
- package/docs/harness/cli/trace-command.md +122 -0
- package/docs/harness/cli-daemon.md +92 -0
- package/docs/harness/event-loop.md +184 -0
- package/docs/harness/hooks/README.md +15 -0
- package/docs/harness/hooks/context-hooks.md +96 -0
- package/docs/harness/hooks/lifecycle-and-observability-hooks.md +135 -0
- package/docs/harness/hooks/validation-hooks.md +97 -0
- package/docs/harness/test-harness.md +149 -0
- package/docs/harness/tui.md +176 -0
- package/docs/memories.md +20 -0
- package/docs/solutions/agentic-issues/premature-agent-deletion-tui-action-dependency-20260130.md +49 -0
- package/docs/solutions/agentic-issues/ref-anchor-scope-mismatch-skill-references-20260131.md +55 -0
- package/docs/solutions/agentic-issues/tautological-tests-routing-20260131.md +52 -0
- package/docs/solutions/integration_issue/blocktool-output-format-mismatch-hook-runner-20260130.md +52 -0
- package/docs/solutions/integration_issue/dual-validation-path-divergence-schema-20260130.md +66 -0
- package/docs/solutions/security-issues/unsanitized-domain-path-join-20260131.md +52 -0
- package/docs/solutions/test-failures/event-loop-mock-ordering-checkAgentWindows-20260130.md +63 -0
- package/docs/sync-cli/README.md +19 -0
- package/docs/sync-cli/cli-entrypoint-and-commands.md +39 -0
- package/docs/sync-cli/commands/README.md +11 -0
- package/docs/sync-cli/commands/pull-manifest-command.md +36 -0
- package/docs/sync-cli/commands/push-command.md +84 -0
- package/docs/sync-cli/commands/sync-command.md +71 -0
- package/docs/sync-cli/systems/README.md +14 -0
- package/docs/sync-cli/systems/git-and-github-integration.md +49 -0
- package/docs/sync-cli/systems/interactive-ui.md +43 -0
- package/docs/sync-cli/systems/manifest-and-distribution.md +51 -0
- package/docs/sync-cli/systems/path-resolution.md +42 -0
- package/package.json +46 -0
- package/scripts/install-shim.sh +40 -0
- package/scripts/pre-pack.sh +25 -0
- package/specs/harness-maintenance-skill.spec.md +138 -0
- package/specs/roadmap/git-spec-lifecycle-management.spec.md +113 -0
- package/specs/sync-init-flag.spec.md +117 -0
- package/specs/unified-workflow-orchestration.spec.md +250 -0
- package/specs/validation-tooling-practice.spec.md +98 -0
- package/specs/workflow-domain-configuration.spec.md +265 -0
- package/src/commands/pull-manifest.ts +31 -0
- package/src/commands/push.ts +344 -0
- package/src/commands/sync.ts +289 -0
- package/src/lib/constants.ts +10 -0
- package/src/lib/dotfiles.ts +36 -0
- package/src/lib/fs-utils.ts +18 -0
- package/src/lib/gh.ts +40 -0
- package/src/lib/git.ts +63 -0
- package/src/lib/gitignore.ts +167 -0
- package/src/lib/manifest.ts +121 -0
- package/src/lib/marker-sync.ts +39 -0
- package/src/lib/paths.ts +38 -0
- package/src/lib/target-lines.ts +66 -0
- package/src/lib/ui.ts +78 -0
- package/src/sync-cli.ts +120 -0
- package/target-lines.json +23 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,861 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit Tests - Schema Validation Library
|
|
3
|
+
*
|
|
4
|
+
* Tests the core schema validation functions including:
|
|
5
|
+
* - validateField() for all type branches (string, integer, boolean, date, enum, array, object)
|
|
6
|
+
* - Array item-type validation (added in validation-tooling-practice Prompt 04)
|
|
7
|
+
* - Schema loading and listing
|
|
8
|
+
* - Frontmatter extraction and validation
|
|
9
|
+
* - Schema type detection
|
|
10
|
+
* - Default application and error formatting
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect } from 'vitest';
|
|
14
|
+
import {
|
|
15
|
+
loadSchema,
|
|
16
|
+
listSchemas,
|
|
17
|
+
extractFrontmatter,
|
|
18
|
+
validateFrontmatter,
|
|
19
|
+
validateFile,
|
|
20
|
+
applyDefaults,
|
|
21
|
+
formatErrors,
|
|
22
|
+
detectSchemaType,
|
|
23
|
+
inferSchemaType,
|
|
24
|
+
type SchemaField,
|
|
25
|
+
type Schema,
|
|
26
|
+
type ValidationResult,
|
|
27
|
+
} from '../schema.js';
|
|
28
|
+
|
|
29
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
30
|
+
// Helpers
|
|
31
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Build a minimal schema with a single field for isolated validateField testing.
|
|
35
|
+
* validateFrontmatter delegates to validateField per-field, so we test through
|
|
36
|
+
* the public API by constructing single-field schemas.
|
|
37
|
+
*/
|
|
38
|
+
function schemaWith(fieldName: string, field: SchemaField): Schema {
|
|
39
|
+
return { frontmatter: { [fieldName]: field } };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function validate(fieldName: string, field: SchemaField, value: unknown): ValidationResult {
|
|
43
|
+
const schema = schemaWith(fieldName, field);
|
|
44
|
+
const frontmatter: Record<string, unknown> = {};
|
|
45
|
+
if (value !== undefined) {
|
|
46
|
+
frontmatter[fieldName] = value;
|
|
47
|
+
}
|
|
48
|
+
return validateFrontmatter(frontmatter, schema);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
52
|
+
// validateField — Type Branches
|
|
53
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
describe('validateField via validateFrontmatter', () => {
|
|
56
|
+
// --- Required / Optional ---
|
|
57
|
+
|
|
58
|
+
describe('required and optional fields', () => {
|
|
59
|
+
it('returns error when required field is missing', () => {
|
|
60
|
+
const result = validate('name', { type: 'string', required: true }, undefined);
|
|
61
|
+
expect(result.valid).toBe(false);
|
|
62
|
+
expect(result.errors).toHaveLength(1);
|
|
63
|
+
expect(result.errors[0].field).toBe('name');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('returns valid when optional field is missing', () => {
|
|
67
|
+
const result = validate('name', { type: 'string', required: false }, undefined);
|
|
68
|
+
expect(result.valid).toBe(true);
|
|
69
|
+
expect(result.errors).toHaveLength(0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('returns valid when optional field with no required flag is missing', () => {
|
|
73
|
+
const result = validate('name', { type: 'string' }, undefined);
|
|
74
|
+
expect(result.valid).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// --- String ---
|
|
79
|
+
|
|
80
|
+
describe('string type', () => {
|
|
81
|
+
it('accepts a valid string', () => {
|
|
82
|
+
const result = validate('title', { type: 'string', required: true }, 'hello');
|
|
83
|
+
expect(result.valid).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('accepts an empty string', () => {
|
|
87
|
+
const result = validate('title', { type: 'string', required: true }, '');
|
|
88
|
+
expect(result.valid).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('rejects a number', () => {
|
|
92
|
+
const result = validate('title', { type: 'string', required: true }, 42);
|
|
93
|
+
expect(result.valid).toBe(false);
|
|
94
|
+
expect(result.errors[0].field).toBe('title');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('rejects a boolean', () => {
|
|
98
|
+
const result = validate('title', { type: 'string', required: true }, true);
|
|
99
|
+
expect(result.valid).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// --- Integer ---
|
|
104
|
+
|
|
105
|
+
describe('integer type', () => {
|
|
106
|
+
it('accepts a valid integer', () => {
|
|
107
|
+
const result = validate('count', { type: 'integer', required: true }, 5);
|
|
108
|
+
expect(result.valid).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('accepts zero', () => {
|
|
112
|
+
const result = validate('count', { type: 'integer', required: true }, 0);
|
|
113
|
+
expect(result.valid).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('accepts negative integer', () => {
|
|
117
|
+
const result = validate('count', { type: 'integer', required: true }, -3);
|
|
118
|
+
expect(result.valid).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('rejects a float', () => {
|
|
122
|
+
const result = validate('count', { type: 'integer', required: true }, 3.14);
|
|
123
|
+
expect(result.valid).toBe(false);
|
|
124
|
+
expect(result.errors[0].field).toBe('count');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('rejects a string', () => {
|
|
128
|
+
const result = validate('count', { type: 'integer', required: true }, '5');
|
|
129
|
+
expect(result.valid).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// --- Boolean ---
|
|
134
|
+
|
|
135
|
+
describe('boolean type', () => {
|
|
136
|
+
it('accepts true', () => {
|
|
137
|
+
const result = validate('flag', { type: 'boolean', required: true }, true);
|
|
138
|
+
expect(result.valid).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('accepts false', () => {
|
|
142
|
+
const result = validate('flag', { type: 'boolean', required: true }, false);
|
|
143
|
+
expect(result.valid).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('rejects a string', () => {
|
|
147
|
+
const result = validate('flag', { type: 'boolean', required: true }, 'true');
|
|
148
|
+
expect(result.valid).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('rejects a number', () => {
|
|
152
|
+
const result = validate('flag', { type: 'boolean', required: true }, 1);
|
|
153
|
+
expect(result.valid).toBe(false);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// --- Date ---
|
|
158
|
+
|
|
159
|
+
describe('date type', () => {
|
|
160
|
+
it('accepts a valid ISO 8601 date', () => {
|
|
161
|
+
const result = validate('created', { type: 'date', required: true }, '2025-01-15');
|
|
162
|
+
expect(result.valid).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('accepts a full ISO datetime', () => {
|
|
166
|
+
const result = validate('created', { type: 'date', required: true }, '2025-01-15T10:30:00Z');
|
|
167
|
+
expect(result.valid).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('rejects an invalid date string', () => {
|
|
171
|
+
const result = validate('created', { type: 'date', required: true }, 'not-a-date');
|
|
172
|
+
expect(result.valid).toBe(false);
|
|
173
|
+
expect(result.errors[0].field).toBe('created');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('rejects a number', () => {
|
|
177
|
+
const result = validate('created', { type: 'date', required: true }, 1705334400000);
|
|
178
|
+
expect(result.valid).toBe(false);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// --- Enum ---
|
|
183
|
+
|
|
184
|
+
describe('enum type', () => {
|
|
185
|
+
const enumField: SchemaField = {
|
|
186
|
+
type: 'enum',
|
|
187
|
+
required: true,
|
|
188
|
+
values: ['pending', 'in_progress', 'done'],
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
it('accepts a valid enum value', () => {
|
|
192
|
+
const result = validate('status', enumField, 'pending');
|
|
193
|
+
expect(result.valid).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('accepts another valid enum value', () => {
|
|
197
|
+
const result = validate('status', enumField, 'done');
|
|
198
|
+
expect(result.valid).toBe(true);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('rejects an invalid enum value', () => {
|
|
202
|
+
const result = validate('status', enumField, 'invalid_status');
|
|
203
|
+
expect(result.valid).toBe(false);
|
|
204
|
+
expect(result.errors[0].field).toBe('status');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('rejects an empty string not in values', () => {
|
|
208
|
+
const result = validate('status', enumField, '');
|
|
209
|
+
expect(result.valid).toBe(false);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// --- Array ---
|
|
214
|
+
|
|
215
|
+
describe('array type', () => {
|
|
216
|
+
it('accepts a valid array', () => {
|
|
217
|
+
const result = validate('tags', { type: 'array', required: true }, ['a', 'b']);
|
|
218
|
+
expect(result.valid).toBe(true);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('accepts an empty array', () => {
|
|
222
|
+
const result = validate('tags', { type: 'array', required: true }, []);
|
|
223
|
+
expect(result.valid).toBe(true);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('rejects a non-array value', () => {
|
|
227
|
+
const result = validate('tags', { type: 'array', required: true }, 'not-array');
|
|
228
|
+
expect(result.valid).toBe(false);
|
|
229
|
+
expect(result.errors[0].field).toBe('tags');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('rejects an object (not an array)', () => {
|
|
233
|
+
const result = validate('tags', { type: 'array', required: true }, { key: 'val' });
|
|
234
|
+
expect(result.valid).toBe(false);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Array item-type validation (Prompt 04 addition)
|
|
238
|
+
describe('item-type validation', () => {
|
|
239
|
+
it('accepts string array when items: string', () => {
|
|
240
|
+
const result = validate('tools', { type: 'array', required: true, items: 'string' }, ['playwright', 'vitest']);
|
|
241
|
+
expect(result.valid).toBe(true);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('rejects non-string items when items: string', () => {
|
|
245
|
+
const result = validate('tools', { type: 'array', required: true, items: 'string' }, [123, 'valid']);
|
|
246
|
+
expect(result.valid).toBe(false);
|
|
247
|
+
expect(result.errors[0].field).toBe('tools');
|
|
248
|
+
expect(result.errors[0].message).toContain('non-string');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('accepts integer array when items: integer', () => {
|
|
252
|
+
const result = validate('deps', { type: 'array', required: true, items: 'integer' }, [1, 2, 3]);
|
|
253
|
+
expect(result.valid).toBe(true);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('rejects float in integer array', () => {
|
|
257
|
+
const result = validate('deps', { type: 'array', required: true, items: 'integer' }, [1, 2.5, 3]);
|
|
258
|
+
expect(result.valid).toBe(false);
|
|
259
|
+
expect(result.errors[0].message).toContain('non-integer');
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('rejects string in integer array', () => {
|
|
263
|
+
const result = validate('deps', { type: 'array', required: true, items: 'integer' }, [1, '2', 3]);
|
|
264
|
+
expect(result.valid).toBe(false);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('accepts empty array with items constraint', () => {
|
|
268
|
+
const result = validate('tools', { type: 'array', required: true, items: 'string' }, []);
|
|
269
|
+
expect(result.valid).toBe(true);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('accepts array without items constraint (no type checking)', () => {
|
|
273
|
+
const result = validate('mixed', { type: 'array', required: true }, [1, 'two', true]);
|
|
274
|
+
expect(result.valid).toBe(true);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// --- Object ---
|
|
280
|
+
|
|
281
|
+
describe('object type', () => {
|
|
282
|
+
it('accepts a valid object', () => {
|
|
283
|
+
const result = validate('config', { type: 'object', required: true }, { key: 'val' });
|
|
284
|
+
expect(result.valid).toBe(true);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('rejects null', () => {
|
|
288
|
+
// null is present but not a valid object — required field with null value
|
|
289
|
+
// In validateField, null triggers the required check first
|
|
290
|
+
const schema = schemaWith('config', { type: 'object', required: true });
|
|
291
|
+
const r = validateFrontmatter({ config: null }, schema);
|
|
292
|
+
expect(r.valid).toBe(false);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('rejects an array (which is typeof object)', () => {
|
|
296
|
+
const result = validate('config', { type: 'object', required: true }, [1, 2]);
|
|
297
|
+
expect(result.valid).toBe(false);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('rejects a string', () => {
|
|
301
|
+
const result = validate('config', { type: 'object', required: true }, 'not-object');
|
|
302
|
+
expect(result.valid).toBe(false);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe('nested property validation', () => {
|
|
306
|
+
const nestedField: SchemaField = {
|
|
307
|
+
type: 'object',
|
|
308
|
+
required: true,
|
|
309
|
+
properties: {
|
|
310
|
+
name: { type: 'string', required: true },
|
|
311
|
+
count: { type: 'integer', required: false },
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
it('accepts object with valid nested properties', () => {
|
|
316
|
+
const result = validate('config', nestedField, { name: 'test', count: 5 });
|
|
317
|
+
expect(result.valid).toBe(true);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('accepts object with optional nested property missing', () => {
|
|
321
|
+
const result = validate('config', nestedField, { name: 'test' });
|
|
322
|
+
expect(result.valid).toBe(true);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('rejects object with missing required nested property', () => {
|
|
326
|
+
const result = validate('config', nestedField, { count: 5 });
|
|
327
|
+
expect(result.valid).toBe(false);
|
|
328
|
+
expect(result.errors[0].field).toBe('config.name');
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('rejects object with wrong-type nested property', () => {
|
|
332
|
+
const result = validate('config', nestedField, { name: 123, count: 5 });
|
|
333
|
+
expect(result.valid).toBe(false);
|
|
334
|
+
expect(result.errors[0].field).toBe('config.name');
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
341
|
+
// Schema Loading & Listing
|
|
342
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
343
|
+
|
|
344
|
+
describe('loadSchema', () => {
|
|
345
|
+
it('returns a schema object for known type "prompt"', () => {
|
|
346
|
+
const schema = loadSchema('prompt');
|
|
347
|
+
expect(schema).not.toBeNull();
|
|
348
|
+
expect(schema!.frontmatter).toBeDefined();
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('returns a schema object for "validation-suite"', () => {
|
|
352
|
+
const schema = loadSchema('validation-suite');
|
|
353
|
+
expect(schema).not.toBeNull();
|
|
354
|
+
expect(schema!.frontmatter).toBeDefined();
|
|
355
|
+
expect(schema!.frontmatter!['tools']).toBeDefined();
|
|
356
|
+
expect(schema!.frontmatter!['tools'].type).toBe('array');
|
|
357
|
+
expect(schema!.frontmatter!['tools'].items).toBe('string');
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('returns a schema object for "workflow"', () => {
|
|
361
|
+
const schema = loadSchema('workflow');
|
|
362
|
+
expect(schema).not.toBeNull();
|
|
363
|
+
expect(schema!.frontmatter).toBeDefined();
|
|
364
|
+
expect(schema!.frontmatter!['name']).toBeDefined();
|
|
365
|
+
expect(schema!.frontmatter!['type'].type).toBe('enum');
|
|
366
|
+
expect(schema!.frontmatter!['planning_depth'].type).toBe('enum');
|
|
367
|
+
expect(schema!.frontmatter!['jury_required'].type).toBe('boolean');
|
|
368
|
+
expect(schema!.frontmatter!['max_tangential_hypotheses'].type).toBe('integer');
|
|
369
|
+
expect(schema!.frontmatter!['required_ideation_questions'].type).toBe('array');
|
|
370
|
+
expect(schema!.frontmatter!['required_ideation_questions'].items).toBe('string');
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('returns null for unknown schema type', () => {
|
|
374
|
+
const schema = loadSchema('nonexistent-schema-type');
|
|
375
|
+
expect(schema).toBeNull();
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('caches schema on subsequent calls', () => {
|
|
379
|
+
const first = loadSchema('prompt');
|
|
380
|
+
const second = loadSchema('prompt');
|
|
381
|
+
expect(first).toBe(second); // same reference
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('returns null consistently for nonexistent type (no stale cache)', () => {
|
|
385
|
+
// Verify that looking up a nonexistent type multiple times
|
|
386
|
+
// always returns null and doesn't corrupt the cache
|
|
387
|
+
const first = loadSchema('totally-fake-schema');
|
|
388
|
+
const second = loadSchema('totally-fake-schema');
|
|
389
|
+
expect(first).toBeNull();
|
|
390
|
+
expect(second).toBeNull();
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('does not cross-contaminate cache between different schema types', () => {
|
|
394
|
+
const prompt = loadSchema('prompt');
|
|
395
|
+
const suite = loadSchema('validation-suite');
|
|
396
|
+
expect(prompt).not.toBeNull();
|
|
397
|
+
expect(suite).not.toBeNull();
|
|
398
|
+
// They should be different objects (different schema definitions)
|
|
399
|
+
expect(prompt).not.toBe(suite);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('cached schema retains full structure on repeated access', () => {
|
|
403
|
+
// Load once to populate cache, then verify structure is intact on cache hit
|
|
404
|
+
loadSchema('prompt'); // warm cache
|
|
405
|
+
const cached = loadSchema('prompt');
|
|
406
|
+
expect(cached).not.toBeNull();
|
|
407
|
+
expect(cached!.frontmatter).toBeDefined();
|
|
408
|
+
expect(cached!.frontmatter!['number']).toBeDefined();
|
|
409
|
+
expect(cached!.frontmatter!['number'].type).toBe('integer');
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
describe('listSchemas', () => {
|
|
414
|
+
it('returns an array of schema type strings', () => {
|
|
415
|
+
const schemas = listSchemas();
|
|
416
|
+
expect(Array.isArray(schemas)).toBe(true);
|
|
417
|
+
expect(schemas.length).toBeGreaterThan(0);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('includes known schema types', () => {
|
|
421
|
+
const schemas = listSchemas();
|
|
422
|
+
expect(schemas).toContain('prompt');
|
|
423
|
+
expect(schemas).toContain('validation-suite');
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
428
|
+
// Frontmatter Extraction
|
|
429
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
430
|
+
|
|
431
|
+
describe('extractFrontmatter', () => {
|
|
432
|
+
it('parses valid YAML frontmatter', () => {
|
|
433
|
+
const content = `---
|
|
434
|
+
title: Hello
|
|
435
|
+
count: 5
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
Body content here.`;
|
|
439
|
+
const result = extractFrontmatter(content);
|
|
440
|
+
expect(result.frontmatter).not.toBeNull();
|
|
441
|
+
expect(result.frontmatter!['title']).toBe('Hello');
|
|
442
|
+
expect(result.frontmatter!['count']).toBe(5);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('separates body content correctly', () => {
|
|
446
|
+
const content = `---
|
|
447
|
+
key: value
|
|
448
|
+
---
|
|
449
|
+
|
|
450
|
+
# Body
|
|
451
|
+
|
|
452
|
+
Some text.`;
|
|
453
|
+
const result = extractFrontmatter(content);
|
|
454
|
+
expect(result.body).toContain('# Body');
|
|
455
|
+
expect(result.body).toContain('Some text.');
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('returns null frontmatter for content without delimiters', () => {
|
|
459
|
+
const content = '# Just a heading\n\nSome text.';
|
|
460
|
+
const result = extractFrontmatter(content);
|
|
461
|
+
expect(result.frontmatter).toBeNull();
|
|
462
|
+
expect(result.body).toBe(content);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it('returns null frontmatter for malformed YAML', () => {
|
|
466
|
+
const content = `---
|
|
467
|
+
: : : invalid yaml [[[
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
Body.`;
|
|
471
|
+
const result = extractFrontmatter(content);
|
|
472
|
+
// parseYaml may or may not throw depending on how malformed — verify graceful handling
|
|
473
|
+
expect(result.body).toBeDefined();
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it('handles empty frontmatter', () => {
|
|
477
|
+
const content = `---
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
Body only.`;
|
|
481
|
+
const result = extractFrontmatter(content);
|
|
482
|
+
// Empty YAML parses to null in some parsers
|
|
483
|
+
expect(result.body).toBeDefined();
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
488
|
+
// extractFrontmatter — Boundary Conditions (Stability)
|
|
489
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
490
|
+
|
|
491
|
+
describe('extractFrontmatter boundary conditions', () => {
|
|
492
|
+
it('returns null frontmatter for empty string input', () => {
|
|
493
|
+
const result = extractFrontmatter('');
|
|
494
|
+
expect(result.frontmatter).toBeNull();
|
|
495
|
+
expect(result.body).toBe('');
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('returns null frontmatter for only frontmatter delimiters with empty YAML', () => {
|
|
499
|
+
// "---\n---\n" has empty YAML between delimiters
|
|
500
|
+
// parseYaml('') returns null — extractFrontmatter should handle gracefully
|
|
501
|
+
const content = '---\n---\n';
|
|
502
|
+
const result = extractFrontmatter(content);
|
|
503
|
+
// The regex matches but parseYaml on empty string returns null,
|
|
504
|
+
// which is cast to Record<string, unknown> — may be null
|
|
505
|
+
expect(result.body).toBeDefined();
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it('handles content ending at closing --- with no trailing newline', () => {
|
|
509
|
+
// Regex: /^---\n([\s\S]*?)\n---\n([\s\S]*)$/
|
|
510
|
+
// Content "---\nkey: val\n---" has no trailing \n after closing ---
|
|
511
|
+
// This means the regex will NOT match (requires \n after closing ---)
|
|
512
|
+
// DIVERGENCE: hooks parseFrontmatter regex /^---\n([\s\S]*?)\n---/ DOES match this
|
|
513
|
+
const content = '---\nkey: val\n---';
|
|
514
|
+
const result = extractFrontmatter(content);
|
|
515
|
+
expect(result.frontmatter).toBeNull();
|
|
516
|
+
expect(result.body).toBe(content);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it('handles embedded --- in YAML string values', () => {
|
|
520
|
+
// Non-greedy ([\s\S]*?) should stop at first literal \n---\n
|
|
521
|
+
// The quoted "---" inside a value should not confuse the regex
|
|
522
|
+
const content = '---\nseparator: "---"\ntitle: test\n---\n\nBody text.';
|
|
523
|
+
const result = extractFrontmatter(content);
|
|
524
|
+
expect(result.frontmatter).not.toBeNull();
|
|
525
|
+
expect(result.frontmatter!['title']).toBe('test');
|
|
526
|
+
expect(result.body).toContain('Body text.');
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it('returns null frontmatter for content with only opening ---', () => {
|
|
530
|
+
const content = '---\nkey: value\nmore: stuff';
|
|
531
|
+
const result = extractFrontmatter(content);
|
|
532
|
+
expect(result.frontmatter).toBeNull();
|
|
533
|
+
expect(result.body).toBe(content);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it('captures first --- block only, not body content with triple-dash', () => {
|
|
537
|
+
const content = '---\nfoo: bar\n---\n\nBody\n\n---\n\nMore body.';
|
|
538
|
+
const result = extractFrontmatter(content);
|
|
539
|
+
expect(result.frontmatter).not.toBeNull();
|
|
540
|
+
expect(result.frontmatter!['foo']).toBe('bar');
|
|
541
|
+
// Body should contain everything after the first closing ---\n
|
|
542
|
+
expect(result.body).toContain('Body');
|
|
543
|
+
expect(result.body).toContain('More body.');
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it('returns null frontmatter when content starts with whitespace before ---', () => {
|
|
547
|
+
// Regex anchors with ^--- so leading whitespace prevents match
|
|
548
|
+
const content = ' ---\nkey: val\n---\n\nBody.';
|
|
549
|
+
const result = extractFrontmatter(content);
|
|
550
|
+
expect(result.frontmatter).toBeNull();
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
555
|
+
// validateFrontmatter — Multi-field
|
|
556
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
557
|
+
|
|
558
|
+
describe('validateFrontmatter', () => {
|
|
559
|
+
it('returns valid for conforming frontmatter', () => {
|
|
560
|
+
const schema: Schema = {
|
|
561
|
+
frontmatter: {
|
|
562
|
+
name: { type: 'string', required: true },
|
|
563
|
+
count: { type: 'integer', required: false, default: 0 },
|
|
564
|
+
},
|
|
565
|
+
};
|
|
566
|
+
const result = validateFrontmatter({ name: 'test' }, schema);
|
|
567
|
+
expect(result.valid).toBe(true);
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it('collects multiple errors', () => {
|
|
571
|
+
const schema: Schema = {
|
|
572
|
+
frontmatter: {
|
|
573
|
+
name: { type: 'string', required: true },
|
|
574
|
+
status: { type: 'enum', required: true, values: ['a', 'b'] },
|
|
575
|
+
},
|
|
576
|
+
};
|
|
577
|
+
const result = validateFrontmatter({}, schema);
|
|
578
|
+
expect(result.valid).toBe(false);
|
|
579
|
+
expect(result.errors.length).toBeGreaterThanOrEqual(2);
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
it('uses schema.fields fallback when frontmatter is absent', () => {
|
|
583
|
+
const schema: Schema = {
|
|
584
|
+
fields: {
|
|
585
|
+
name: { type: 'string', required: true },
|
|
586
|
+
},
|
|
587
|
+
};
|
|
588
|
+
const result = validateFrontmatter({}, schema);
|
|
589
|
+
expect(result.valid).toBe(false);
|
|
590
|
+
expect(result.errors[0].field).toBe('name');
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it('returns valid for empty schema', () => {
|
|
594
|
+
const result = validateFrontmatter({ anything: 'goes' }, {});
|
|
595
|
+
expect(result.valid).toBe(true);
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
600
|
+
// validateFile — Integration
|
|
601
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
602
|
+
|
|
603
|
+
describe('validateFile', () => {
|
|
604
|
+
it('returns error for unknown schema type', () => {
|
|
605
|
+
const result = validateFile('---\nfoo: bar\n---\nBody', 'nonexistent');
|
|
606
|
+
expect(result.valid).toBe(false);
|
|
607
|
+
expect(result.errors[0].field).toBe('_schema');
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it('returns error for missing frontmatter', () => {
|
|
611
|
+
const result = validateFile('Just body content', 'prompt');
|
|
612
|
+
expect(result.valid).toBe(false);
|
|
613
|
+
expect(result.errors[0].field).toBe('_frontmatter');
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
it('does not crash on empty string content with valid schema type', () => {
|
|
617
|
+
const result = validateFile('', 'prompt');
|
|
618
|
+
expect(result.valid).toBe(false);
|
|
619
|
+
// Empty string has no frontmatter, so should get _frontmatter error
|
|
620
|
+
expect(result.errors[0].field).toBe('_frontmatter');
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it('returns _frontmatter error for content with only body (no delimiters)', () => {
|
|
624
|
+
const result = validateFile('# Just a heading\n\nSome body text.', 'prompt');
|
|
625
|
+
expect(result.valid).toBe(false);
|
|
626
|
+
expect(result.errors[0].field).toBe('_frontmatter');
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
it('validates valid frontmatter against a real schema type', () => {
|
|
630
|
+
// Minimal valid prompt content (all required fields present)
|
|
631
|
+
const content = `---
|
|
632
|
+
number: 1
|
|
633
|
+
title: "Test Task"
|
|
634
|
+
type: planned
|
|
635
|
+
status: pending
|
|
636
|
+
---
|
|
637
|
+
|
|
638
|
+
## Tasks
|
|
639
|
+
|
|
640
|
+
- Do something
|
|
641
|
+
|
|
642
|
+
## Acceptance Criteria
|
|
643
|
+
|
|
644
|
+
- Something works
|
|
645
|
+
`;
|
|
646
|
+
const result = validateFile(content, 'prompt');
|
|
647
|
+
expect(result.valid).toBe(true);
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
it('returns valid when schema has no frontmatter or fields keys', () => {
|
|
651
|
+
// validateFrontmatter iterates schema.frontmatter || schema.fields || {}
|
|
652
|
+
// An empty schema means zero fields to validate, so everything passes
|
|
653
|
+
const schema: Schema = {};
|
|
654
|
+
const result = validateFrontmatter({ anything: 'goes', extra: 42 }, schema);
|
|
655
|
+
expect(result.valid).toBe(true);
|
|
656
|
+
expect(result.errors).toHaveLength(0);
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
it('silently passes extra fields not defined in schema', () => {
|
|
660
|
+
const schema: Schema = {
|
|
661
|
+
frontmatter: {
|
|
662
|
+
name: { type: 'string', required: true },
|
|
663
|
+
},
|
|
664
|
+
};
|
|
665
|
+
// 'unknown_field' is not in schema — should be ignored, not rejected
|
|
666
|
+
const result = validateFrontmatter({ name: 'valid', unknown_field: 'extra' }, schema);
|
|
667
|
+
expect(result.valid).toBe(true);
|
|
668
|
+
expect(result.errors).toHaveLength(0);
|
|
669
|
+
});
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
673
|
+
// Schema Type Detection
|
|
674
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
675
|
+
|
|
676
|
+
describe('detectSchemaType', () => {
|
|
677
|
+
it('detects prompt files', () => {
|
|
678
|
+
expect(detectSchemaType('.planning/my-spec/prompts/01.md')).toBe('prompt');
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
it('detects alignment files', () => {
|
|
682
|
+
expect(detectSchemaType('.planning/my-spec/alignment.md')).toBe('alignment');
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
it('detects spec files', () => {
|
|
686
|
+
expect(detectSchemaType('specs/api.spec.md')).toBe('spec');
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it('detects spec files in roadmap subdirectory', () => {
|
|
690
|
+
expect(detectSchemaType('specs/roadmap/feature.spec.md')).toBe('spec');
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
it('detects documentation files', () => {
|
|
694
|
+
expect(detectSchemaType('docs/guide.md')).toBe('documentation');
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it('detects validation-suite files', () => {
|
|
698
|
+
expect(detectSchemaType('.allhands/validation/browser-automation.md')).toBe('validation-suite');
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
it('detects skill files', () => {
|
|
702
|
+
expect(detectSchemaType('.allhands/skills/my-skill/SKILL.md')).toBe('skill');
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
it('detects workflow files', () => {
|
|
706
|
+
expect(detectSchemaType('.allhands/workflows/milestone.md')).toBe('workflow');
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
it('returns null for non-schema files', () => {
|
|
710
|
+
expect(detectSchemaType('README.md')).toBeNull();
|
|
711
|
+
expect(detectSchemaType('src/index.ts')).toBeNull();
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it('strips projectDir prefix before matching', () => {
|
|
715
|
+
expect(detectSchemaType('/home/user/project/.planning/s1/prompts/01.md', '/home/user/project')).toBe('prompt');
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it('handles path without projectDir prefix gracefully', () => {
|
|
719
|
+
expect(detectSchemaType('.planning/s1/prompts/01.md', '/different/project')).toBe('prompt');
|
|
720
|
+
});
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
describe('inferSchemaType', () => {
|
|
724
|
+
it('infers prompt from path containing /prompts/', () => {
|
|
725
|
+
expect(inferSchemaType('/some/path/prompts/01.md')).toBe('prompt');
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
it('infers prompt from filename matching prompt*.md', () => {
|
|
729
|
+
expect(inferSchemaType('prompt-file.md')).toBe('prompt');
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
it('infers alignment from path containing alignment', () => {
|
|
733
|
+
expect(inferSchemaType('/planning/alignment.md')).toBe('alignment');
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
it('infers spec from path containing /specs/', () => {
|
|
737
|
+
expect(inferSchemaType('/project/specs/api.spec.md')).toBe('spec');
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
it('infers spec from .spec.md extension', () => {
|
|
741
|
+
expect(inferSchemaType('feature.spec.md')).toBe('spec');
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
it('infers documentation from /docs/ path', () => {
|
|
745
|
+
expect(inferSchemaType('/project/docs/guide.md')).toBe('documentation');
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
it('infers validation-suite from /validation/ path', () => {
|
|
749
|
+
expect(inferSchemaType('.allhands/validation/suite.md')).toBe('validation-suite');
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
it('infers skill from /skills/ path with SKILL.md', () => {
|
|
753
|
+
expect(inferSchemaType('.allhands/skills/my-skill/SKILL.md')).toBe('skill');
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
it('infers workflow from /workflows/ path', () => {
|
|
757
|
+
expect(inferSchemaType('.allhands/workflows/milestone.md')).toBe('workflow');
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
it('returns null for unknown paths', () => {
|
|
761
|
+
expect(inferSchemaType('README.md')).toBeNull();
|
|
762
|
+
expect(inferSchemaType('src/lib/utils.ts')).toBeNull();
|
|
763
|
+
});
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
767
|
+
// applyDefaults
|
|
768
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
769
|
+
|
|
770
|
+
describe('applyDefaults', () => {
|
|
771
|
+
const schema: Schema = {
|
|
772
|
+
frontmatter: {
|
|
773
|
+
name: { type: 'string', required: true },
|
|
774
|
+
status: { type: 'enum', required: true, values: ['pending', 'done'], default: 'pending' },
|
|
775
|
+
count: { type: 'integer', default: 0 },
|
|
776
|
+
tags: { type: 'array', default: [] },
|
|
777
|
+
},
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
it('fills missing fields with schema defaults', () => {
|
|
781
|
+
const result = applyDefaults({ name: 'test' }, schema);
|
|
782
|
+
expect(result['status']).toBe('pending');
|
|
783
|
+
expect(result['count']).toBe(0);
|
|
784
|
+
expect(result['tags']).toEqual([]);
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
it('does not overwrite existing values', () => {
|
|
788
|
+
const result = applyDefaults({ name: 'test', status: 'done', count: 5 }, schema);
|
|
789
|
+
expect(result['status']).toBe('done');
|
|
790
|
+
expect(result['count']).toBe(5);
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
it('handles empty frontmatter', () => {
|
|
794
|
+
const result = applyDefaults({}, schema);
|
|
795
|
+
expect(result['status']).toBe('pending');
|
|
796
|
+
expect(result['count']).toBe(0);
|
|
797
|
+
expect(result['tags']).toEqual([]);
|
|
798
|
+
expect(result['name']).toBeUndefined(); // no default for name
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
it('uses schema.fields fallback', () => {
|
|
802
|
+
const fieldsSchema: Schema = {
|
|
803
|
+
fields: {
|
|
804
|
+
level: { type: 'integer', default: 1 },
|
|
805
|
+
},
|
|
806
|
+
};
|
|
807
|
+
const result = applyDefaults({}, fieldsSchema);
|
|
808
|
+
expect(result['level']).toBe(1);
|
|
809
|
+
});
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
813
|
+
// formatErrors
|
|
814
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
815
|
+
|
|
816
|
+
describe('formatErrors', () => {
|
|
817
|
+
it('returns "Validation passed" for valid result', () => {
|
|
818
|
+
const result: ValidationResult = { valid: true, errors: [] };
|
|
819
|
+
expect(formatErrors(result)).toBe('Validation passed');
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
it('formats error with field and message', () => {
|
|
823
|
+
const result: ValidationResult = {
|
|
824
|
+
valid: false,
|
|
825
|
+
errors: [{ field: 'status', message: 'Required field is missing' }],
|
|
826
|
+
};
|
|
827
|
+
const output = formatErrors(result);
|
|
828
|
+
expect(output).toContain('status');
|
|
829
|
+
expect(output).toContain('Required field is missing');
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
it('includes expected and received when present', () => {
|
|
833
|
+
const result: ValidationResult = {
|
|
834
|
+
valid: false,
|
|
835
|
+
errors: [{
|
|
836
|
+
field: 'count',
|
|
837
|
+
message: 'Expected integer',
|
|
838
|
+
expected: 'integer',
|
|
839
|
+
received: 'string',
|
|
840
|
+
}],
|
|
841
|
+
};
|
|
842
|
+
const output = formatErrors(result);
|
|
843
|
+
expect(output).toContain('expected: integer');
|
|
844
|
+
expect(output).toContain('got: string');
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
it('formats multiple errors with newlines', () => {
|
|
848
|
+
const result: ValidationResult = {
|
|
849
|
+
valid: false,
|
|
850
|
+
errors: [
|
|
851
|
+
{ field: 'a', message: 'Error A' },
|
|
852
|
+
{ field: 'b', message: 'Error B' },
|
|
853
|
+
],
|
|
854
|
+
};
|
|
855
|
+
const output = formatErrors(result);
|
|
856
|
+
const lines = output.split('\n');
|
|
857
|
+
expect(lines).toHaveLength(2);
|
|
858
|
+
expect(lines[0]).toContain('a');
|
|
859
|
+
expect(lines[1]).toContain('b');
|
|
860
|
+
});
|
|
861
|
+
});
|