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,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Assertion Helpers for Harness Testing
|
|
3
|
+
*
|
|
4
|
+
* Provides domain-specific assertions for CLI and hook testing.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { expect } from 'vitest';
|
|
8
|
+
import type { RunResult } from './cli-runner.js';
|
|
9
|
+
import type { HookResult } from './hook-runner.js';
|
|
10
|
+
import type { TestFixture } from './fixture.js';
|
|
11
|
+
import { readFileSync, existsSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
|
|
14
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
15
|
+
// CLI Result Assertions
|
|
16
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Assert that a CLI command succeeded.
|
|
20
|
+
*/
|
|
21
|
+
export function assertSuccess(result: RunResult, message?: string): void {
|
|
22
|
+
expect(result.success, message ?? `Expected success but got exit code ${result.exitCode}: ${result.stderr}`).toBe(true);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Assert that a CLI command failed.
|
|
27
|
+
*/
|
|
28
|
+
export function assertFailure(result: RunResult, message?: string): void {
|
|
29
|
+
expect(result.success, message ?? `Expected failure but command succeeded`).toBe(false);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Assert stdout contains expected text.
|
|
34
|
+
*/
|
|
35
|
+
export function assertStdoutContains(result: RunResult, expected: string, message?: string): void {
|
|
36
|
+
expect(
|
|
37
|
+
result.stdout.includes(expected),
|
|
38
|
+
message ?? `Expected stdout to contain "${expected}"\nActual stdout:\n${result.stdout}`
|
|
39
|
+
).toBe(true);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Assert stderr contains expected text.
|
|
44
|
+
*/
|
|
45
|
+
export function assertStderrContains(result: RunResult, expected: string, message?: string): void {
|
|
46
|
+
expect(
|
|
47
|
+
result.stderr.includes(expected),
|
|
48
|
+
message ?? `Expected stderr to contain "${expected}"\nActual stderr:\n${result.stderr}`
|
|
49
|
+
).toBe(true);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Assert stdout matches a regex.
|
|
54
|
+
*/
|
|
55
|
+
export function assertStdoutMatches(result: RunResult, pattern: RegExp, message?: string): void {
|
|
56
|
+
expect(
|
|
57
|
+
pattern.test(result.stdout),
|
|
58
|
+
message ?? `Expected stdout to match ${pattern}\nActual stdout:\n${result.stdout}`
|
|
59
|
+
).toBe(true);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Assert JSON output has expected structure.
|
|
64
|
+
*/
|
|
65
|
+
export function assertJsonOutput<T>(
|
|
66
|
+
result: RunResult,
|
|
67
|
+
validator: (json: T) => boolean,
|
|
68
|
+
message?: string
|
|
69
|
+
): void {
|
|
70
|
+
expect(result.json, 'Expected JSON output but none was parsed').toBeDefined();
|
|
71
|
+
expect(
|
|
72
|
+
validator(result.json as T),
|
|
73
|
+
message ?? `JSON output validation failed: ${JSON.stringify(result.json, null, 2)}`
|
|
74
|
+
).toBe(true);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Assert command completed within time limit.
|
|
79
|
+
*/
|
|
80
|
+
export function assertTimedWithin(result: RunResult, maxMs: number, message?: string): void {
|
|
81
|
+
expect(
|
|
82
|
+
result.duration <= maxMs,
|
|
83
|
+
message ?? `Expected completion within ${maxMs}ms but took ${result.duration}ms`
|
|
84
|
+
).toBe(true);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
88
|
+
// Hook Result Assertions
|
|
89
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Assert that a hook allowed the tool.
|
|
93
|
+
*/
|
|
94
|
+
export function assertHookAllowed(result: HookResult, message?: string): void {
|
|
95
|
+
expect(
|
|
96
|
+
result.allowed,
|
|
97
|
+
message ?? `Expected hook to allow tool but it was ${result.denied ? 'denied' : 'blocked'}: ${result.denialReason ?? result.stderr}`
|
|
98
|
+
).toBe(true);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Assert that a hook denied the tool.
|
|
103
|
+
*/
|
|
104
|
+
export function assertHookDenied(result: HookResult, message?: string): void {
|
|
105
|
+
expect(
|
|
106
|
+
result.denied,
|
|
107
|
+
message ?? `Expected hook to deny tool but it was allowed`
|
|
108
|
+
).toBe(true);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Assert that a hook blocked the tool (PostToolUse).
|
|
113
|
+
*/
|
|
114
|
+
export function assertHookBlocked(result: HookResult, message?: string): void {
|
|
115
|
+
expect(
|
|
116
|
+
result.blocked,
|
|
117
|
+
message ?? `Expected hook to block tool but it was allowed`
|
|
118
|
+
).toBe(true);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Assert that a hook injected context.
|
|
123
|
+
*/
|
|
124
|
+
export function assertHookInjectedContext(result: HookResult, message?: string): void {
|
|
125
|
+
expect(
|
|
126
|
+
result.systemMessage,
|
|
127
|
+
message ?? `Expected hook to inject context (systemMessage) but none was found`
|
|
128
|
+
).toBeDefined();
|
|
129
|
+
expect(result.systemMessage!.length).toBeGreaterThan(0);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Assert that hook context contains expected text.
|
|
134
|
+
*/
|
|
135
|
+
export function assertHookContextContains(result: HookResult, expected: string, message?: string): void {
|
|
136
|
+
assertHookInjectedContext(result);
|
|
137
|
+
expect(
|
|
138
|
+
result.systemMessage!.includes(expected),
|
|
139
|
+
message ?? `Expected systemMessage to contain "${expected}"\nActual:\n${result.systemMessage!.substring(0, 500)}`
|
|
140
|
+
).toBe(true);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Assert denial reason contains expected text.
|
|
145
|
+
*/
|
|
146
|
+
export function assertDenialReasonContains(result: HookResult, expected: string, message?: string): void {
|
|
147
|
+
assertHookDenied(result);
|
|
148
|
+
expect(
|
|
149
|
+
result.denialReason?.includes(expected),
|
|
150
|
+
message ?? `Expected denial reason to contain "${expected}"\nActual: ${result.denialReason}`
|
|
151
|
+
).toBe(true);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
155
|
+
// Fixture Assertions
|
|
156
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Assert that a file exists in the fixture.
|
|
160
|
+
*/
|
|
161
|
+
export function assertFileExists(fixture: TestFixture, relativePath: string, message?: string): void {
|
|
162
|
+
const fullPath = join(fixture.root, relativePath);
|
|
163
|
+
expect(
|
|
164
|
+
existsSync(fullPath),
|
|
165
|
+
message ?? `Expected file to exist: ${relativePath}`
|
|
166
|
+
).toBe(true);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Assert that a file does not exist in the fixture.
|
|
171
|
+
*/
|
|
172
|
+
export function assertFileNotExists(fixture: TestFixture, relativePath: string, message?: string): void {
|
|
173
|
+
const fullPath = join(fixture.root, relativePath);
|
|
174
|
+
expect(
|
|
175
|
+
existsSync(fullPath),
|
|
176
|
+
message ?? `Expected file to not exist: ${relativePath}`
|
|
177
|
+
).toBe(false);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Assert file content contains expected text.
|
|
182
|
+
*/
|
|
183
|
+
export function assertFileContains(
|
|
184
|
+
fixture: TestFixture,
|
|
185
|
+
relativePath: string,
|
|
186
|
+
expected: string,
|
|
187
|
+
message?: string
|
|
188
|
+
): void {
|
|
189
|
+
assertFileExists(fixture, relativePath);
|
|
190
|
+
const content = readFileSync(join(fixture.root, relativePath), 'utf-8');
|
|
191
|
+
expect(
|
|
192
|
+
content.includes(expected),
|
|
193
|
+
message ?? `Expected ${relativePath} to contain "${expected}"\nActual:\n${content.substring(0, 500)}`
|
|
194
|
+
).toBe(true);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Assert file content matches regex.
|
|
199
|
+
*/
|
|
200
|
+
export function assertFileMatches(
|
|
201
|
+
fixture: TestFixture,
|
|
202
|
+
relativePath: string,
|
|
203
|
+
pattern: RegExp,
|
|
204
|
+
message?: string
|
|
205
|
+
): void {
|
|
206
|
+
assertFileExists(fixture, relativePath);
|
|
207
|
+
const content = readFileSync(join(fixture.root, relativePath), 'utf-8');
|
|
208
|
+
expect(
|
|
209
|
+
pattern.test(content),
|
|
210
|
+
message ?? `Expected ${relativePath} to match ${pattern}`
|
|
211
|
+
).toBe(true);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Assert file has valid YAML frontmatter.
|
|
216
|
+
*/
|
|
217
|
+
export function assertValidFrontmatter(
|
|
218
|
+
fixture: TestFixture,
|
|
219
|
+
relativePath: string,
|
|
220
|
+
requiredFields: string[] = []
|
|
221
|
+
): void {
|
|
222
|
+
assertFileExists(fixture, relativePath);
|
|
223
|
+
const content = readFileSync(join(fixture.root, relativePath), 'utf-8');
|
|
224
|
+
|
|
225
|
+
// Check frontmatter exists
|
|
226
|
+
expect(content.startsWith('---'), `Expected ${relativePath} to start with frontmatter`).toBe(true);
|
|
227
|
+
|
|
228
|
+
const endIndex = content.indexOf('---', 3);
|
|
229
|
+
expect(endIndex > 3, `Expected ${relativePath} to have closing frontmatter delimiter`).toBe(true);
|
|
230
|
+
|
|
231
|
+
const frontmatter = content.substring(3, endIndex).trim();
|
|
232
|
+
|
|
233
|
+
// Check required fields
|
|
234
|
+
for (const field of requiredFields) {
|
|
235
|
+
expect(
|
|
236
|
+
frontmatter.includes(`${field}:`),
|
|
237
|
+
`Expected frontmatter to contain "${field}" field`
|
|
238
|
+
).toBe(true);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
243
|
+
// Git Assertions
|
|
244
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Assert file is tracked by git.
|
|
248
|
+
*/
|
|
249
|
+
export async function assertGitTracked(
|
|
250
|
+
fixture: TestFixture,
|
|
251
|
+
relativePath: string,
|
|
252
|
+
message?: string
|
|
253
|
+
): Promise<void> {
|
|
254
|
+
const { execSync } = await import('child_process');
|
|
255
|
+
try {
|
|
256
|
+
execSync(`git ls-files --error-unmatch "${relativePath}"`, {
|
|
257
|
+
cwd: fixture.root,
|
|
258
|
+
stdio: 'pipe',
|
|
259
|
+
});
|
|
260
|
+
} catch {
|
|
261
|
+
expect.fail(message ?? `Expected ${relativePath} to be tracked by git`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Assert file has uncommitted changes.
|
|
267
|
+
*/
|
|
268
|
+
export async function assertGitDirty(
|
|
269
|
+
fixture: TestFixture,
|
|
270
|
+
relativePath: string,
|
|
271
|
+
message?: string
|
|
272
|
+
): Promise<void> {
|
|
273
|
+
const { execSync } = await import('child_process');
|
|
274
|
+
const status = execSync(`git status --porcelain "${relativePath}"`, {
|
|
275
|
+
cwd: fixture.root,
|
|
276
|
+
encoding: 'utf-8',
|
|
277
|
+
});
|
|
278
|
+
expect(
|
|
279
|
+
status.trim().length > 0,
|
|
280
|
+
message ?? `Expected ${relativePath} to have uncommitted changes`
|
|
281
|
+
).toBe(true);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
285
|
+
// Composite Assertions
|
|
286
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Assert a complete command workflow succeeded.
|
|
290
|
+
*/
|
|
291
|
+
export function assertWorkflowSuccess(
|
|
292
|
+
results: { name: string; result: RunResult }[],
|
|
293
|
+
message?: string
|
|
294
|
+
): void {
|
|
295
|
+
const failures = results.filter((r) => !r.result.success);
|
|
296
|
+
if (failures.length > 0) {
|
|
297
|
+
const details = failures
|
|
298
|
+
.map((f) => ` ${f.name}: exit ${f.result.exitCode}, stderr: ${f.result.stderr}`)
|
|
299
|
+
.join('\n');
|
|
300
|
+
expect.fail(message ?? `Workflow had ${failures.length} failures:\n${details}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Assert all hook contracts passed.
|
|
306
|
+
*/
|
|
307
|
+
export function assertContractsPassed(
|
|
308
|
+
results: { contract: { name: string }; passed: boolean; failures: string[] }[],
|
|
309
|
+
message?: string
|
|
310
|
+
): void {
|
|
311
|
+
const failed = results.filter((r) => !r.passed);
|
|
312
|
+
if (failed.length > 0) {
|
|
313
|
+
const details = failed
|
|
314
|
+
.map((f) => ` ${f.contract.name}:\n ${f.failures.join('\n ')}`)
|
|
315
|
+
.join('\n');
|
|
316
|
+
expect.fail(message ?? `${failed.length} contracts failed:\n${details}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Runner - Execute ah commands and capture output
|
|
3
|
+
*
|
|
4
|
+
* Provides utilities for running the CLI headlessly and asserting on results.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn, SpawnOptions, ChildProcess } from 'child_process';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import type { TestFixture } from './fixture.js';
|
|
10
|
+
|
|
11
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12
|
+
// Types
|
|
13
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export interface RunOptions {
|
|
16
|
+
/** Working directory for the command */
|
|
17
|
+
cwd?: string;
|
|
18
|
+
/** Environment variables to set */
|
|
19
|
+
env?: Record<string, string>;
|
|
20
|
+
/** Timeout in milliseconds (default: 30000) */
|
|
21
|
+
timeout?: number;
|
|
22
|
+
/** Input to send to stdin */
|
|
23
|
+
stdin?: string;
|
|
24
|
+
/** Whether to expect JSON output */
|
|
25
|
+
expectJson?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface RunResult {
|
|
29
|
+
/** Exit code (0 = success) */
|
|
30
|
+
exitCode: number;
|
|
31
|
+
/** Stdout as string */
|
|
32
|
+
stdout: string;
|
|
33
|
+
/** Stderr as string */
|
|
34
|
+
stderr: string;
|
|
35
|
+
/** Combined stdout + stderr in order received */
|
|
36
|
+
combined: string;
|
|
37
|
+
/** Whether the command succeeded (exit code 0) */
|
|
38
|
+
success: boolean;
|
|
39
|
+
/** Parsed JSON if expectJson was true and output is valid JSON */
|
|
40
|
+
json?: unknown;
|
|
41
|
+
/** Duration in milliseconds */
|
|
42
|
+
duration: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
46
|
+
// CLI Paths
|
|
47
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get the path to the ah CLI entry point.
|
|
51
|
+
*/
|
|
52
|
+
function getCliPath(): string {
|
|
53
|
+
// From src/__tests__/harness/ go up to harness root
|
|
54
|
+
return join(__dirname, '..', '..', 'cli.ts');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get the tsx executable path.
|
|
59
|
+
*/
|
|
60
|
+
function getTsxPath(): string {
|
|
61
|
+
// Use npx tsx to run TypeScript directly
|
|
62
|
+
return 'npx';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
66
|
+
// Core Runner
|
|
67
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Run an ah CLI command and capture output.
|
|
71
|
+
*/
|
|
72
|
+
export async function runCli(args: string[], options: RunOptions = {}): Promise<RunResult> {
|
|
73
|
+
const {
|
|
74
|
+
cwd = process.cwd(),
|
|
75
|
+
env = {},
|
|
76
|
+
timeout = 30000,
|
|
77
|
+
stdin,
|
|
78
|
+
expectJson = false,
|
|
79
|
+
} = options;
|
|
80
|
+
|
|
81
|
+
const startTime = Date.now();
|
|
82
|
+
const cliPath = getCliPath();
|
|
83
|
+
|
|
84
|
+
return new Promise((resolve) => {
|
|
85
|
+
const spawnEnv: Record<string, string> = {
|
|
86
|
+
...process.env,
|
|
87
|
+
...env,
|
|
88
|
+
// Disable color output for consistent parsing
|
|
89
|
+
NO_COLOR: '1',
|
|
90
|
+
FORCE_COLOR: '0',
|
|
91
|
+
} as Record<string, string>;
|
|
92
|
+
|
|
93
|
+
const spawnOptions: SpawnOptions = {
|
|
94
|
+
cwd,
|
|
95
|
+
env: spawnEnv,
|
|
96
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Use tsx to run the CLI TypeScript directly
|
|
100
|
+
const child: ChildProcess = spawn('npx', ['tsx', cliPath, ...args], spawnOptions);
|
|
101
|
+
|
|
102
|
+
let stdout = '';
|
|
103
|
+
let stderr = '';
|
|
104
|
+
let combined = '';
|
|
105
|
+
let timedOut = false;
|
|
106
|
+
|
|
107
|
+
const timeoutId = setTimeout(() => {
|
|
108
|
+
timedOut = true;
|
|
109
|
+
child.kill('SIGTERM');
|
|
110
|
+
}, timeout);
|
|
111
|
+
|
|
112
|
+
child.stdout?.on('data', (data: Buffer) => {
|
|
113
|
+
const text = data.toString();
|
|
114
|
+
stdout += text;
|
|
115
|
+
combined += text;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
child.stderr?.on('data', (data: Buffer) => {
|
|
119
|
+
const text = data.toString();
|
|
120
|
+
stderr += text;
|
|
121
|
+
combined += text;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (stdin) {
|
|
125
|
+
child.stdin?.write(stdin);
|
|
126
|
+
child.stdin?.end();
|
|
127
|
+
} else {
|
|
128
|
+
child.stdin?.end();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
child.on('close', (code) => {
|
|
132
|
+
clearTimeout(timeoutId);
|
|
133
|
+
const duration = Date.now() - startTime;
|
|
134
|
+
|
|
135
|
+
const result: RunResult = {
|
|
136
|
+
exitCode: timedOut ? -1 : (code ?? 1),
|
|
137
|
+
stdout: stdout.trim(),
|
|
138
|
+
stderr: stderr.trim(),
|
|
139
|
+
combined: combined.trim(),
|
|
140
|
+
success: !timedOut && code === 0,
|
|
141
|
+
duration,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Try to parse JSON if requested
|
|
145
|
+
if (expectJson && stdout.trim()) {
|
|
146
|
+
try {
|
|
147
|
+
result.json = JSON.parse(stdout.trim());
|
|
148
|
+
} catch {
|
|
149
|
+
// Leave json undefined if parsing fails
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
resolve(result);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
child.on('error', (err) => {
|
|
157
|
+
clearTimeout(timeoutId);
|
|
158
|
+
const duration = Date.now() - startTime;
|
|
159
|
+
|
|
160
|
+
resolve({
|
|
161
|
+
exitCode: -1,
|
|
162
|
+
stdout: '',
|
|
163
|
+
stderr: err.message,
|
|
164
|
+
combined: err.message,
|
|
165
|
+
success: false,
|
|
166
|
+
duration,
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Run a command in the context of a test fixture.
|
|
174
|
+
*/
|
|
175
|
+
export async function runInFixture(
|
|
176
|
+
fixture: TestFixture,
|
|
177
|
+
args: string[],
|
|
178
|
+
options: Omit<RunOptions, 'cwd'> = {}
|
|
179
|
+
): Promise<RunResult> {
|
|
180
|
+
return runCli(args, {
|
|
181
|
+
...options,
|
|
182
|
+
cwd: fixture.root,
|
|
183
|
+
env: {
|
|
184
|
+
...fixture.env,
|
|
185
|
+
...options.env,
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
191
|
+
// Convenience Wrappers
|
|
192
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Run a knowledge search command.
|
|
196
|
+
*/
|
|
197
|
+
export async function runKnowledgeSearch(
|
|
198
|
+
query: string,
|
|
199
|
+
fixture: TestFixture,
|
|
200
|
+
options: { path?: string; k?: number } = {}
|
|
201
|
+
): Promise<RunResult> {
|
|
202
|
+
const args = ['knowledge', 'search', query];
|
|
203
|
+
|
|
204
|
+
if (options.path) {
|
|
205
|
+
args.push('--path', options.path);
|
|
206
|
+
}
|
|
207
|
+
if (options.k) {
|
|
208
|
+
args.push('--k', options.k.toString());
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return runInFixture(fixture, args);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Run a validation command.
|
|
216
|
+
* Uses `ah validate file <path>` syntax.
|
|
217
|
+
*/
|
|
218
|
+
export async function runValidate(
|
|
219
|
+
filePath: string,
|
|
220
|
+
fixture: TestFixture
|
|
221
|
+
): Promise<RunResult> {
|
|
222
|
+
// If path is relative, make it absolute relative to fixture
|
|
223
|
+
const { isAbsolute } = await import('path');
|
|
224
|
+
const absPath = isAbsolute(filePath) ? filePath : join(fixture.root, filePath);
|
|
225
|
+
return runInFixture(fixture, ['validate', 'file', absPath], { expectJson: true });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Run a spawn codesearch command.
|
|
230
|
+
*/
|
|
231
|
+
export async function runCodeSearch(
|
|
232
|
+
query: string,
|
|
233
|
+
fixture: TestFixture,
|
|
234
|
+
options: { budget?: number } = {}
|
|
235
|
+
): Promise<RunResult> {
|
|
236
|
+
const args = ['spawn', 'codesearch', query];
|
|
237
|
+
|
|
238
|
+
if (options.budget) {
|
|
239
|
+
args.push('--budget', options.budget.toString());
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return runInFixture(fixture, args, { timeout: 60000 });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Run a tools list command.
|
|
247
|
+
*/
|
|
248
|
+
export async function runToolsList(fixture: TestFixture): Promise<RunResult> {
|
|
249
|
+
return runInFixture(fixture, ['tools', 'list'], { expectJson: true });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Run a specs list command.
|
|
254
|
+
*/
|
|
255
|
+
export async function runSpecsList(fixture: TestFixture): Promise<RunResult> {
|
|
256
|
+
return runInFixture(fixture, ['specs', 'list']);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
260
|
+
// Batch Runner
|
|
261
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
export interface BatchCommand {
|
|
264
|
+
name: string;
|
|
265
|
+
args: string[];
|
|
266
|
+
options?: RunOptions;
|
|
267
|
+
/** Expected outcome for assertions */
|
|
268
|
+
expect?: {
|
|
269
|
+
success?: boolean;
|
|
270
|
+
exitCode?: number;
|
|
271
|
+
stdoutContains?: string[];
|
|
272
|
+
stderrContains?: string[];
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export interface BatchResult {
|
|
277
|
+
command: BatchCommand;
|
|
278
|
+
result: RunResult;
|
|
279
|
+
passed: boolean;
|
|
280
|
+
failures: string[];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Run multiple commands in sequence and collect results.
|
|
285
|
+
*/
|
|
286
|
+
export async function runBatch(
|
|
287
|
+
commands: BatchCommand[],
|
|
288
|
+
fixture: TestFixture
|
|
289
|
+
): Promise<BatchResult[]> {
|
|
290
|
+
const results: BatchResult[] = [];
|
|
291
|
+
|
|
292
|
+
for (const command of commands) {
|
|
293
|
+
const result = await runInFixture(fixture, command.args, command.options);
|
|
294
|
+
const failures: string[] = [];
|
|
295
|
+
|
|
296
|
+
if (command.expect) {
|
|
297
|
+
const { expect: exp } = command;
|
|
298
|
+
|
|
299
|
+
if (exp.success !== undefined && result.success !== exp.success) {
|
|
300
|
+
failures.push(`Expected success=${exp.success}, got ${result.success}`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (exp.exitCode !== undefined && result.exitCode !== exp.exitCode) {
|
|
304
|
+
failures.push(`Expected exitCode=${exp.exitCode}, got ${result.exitCode}`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (exp.stdoutContains) {
|
|
308
|
+
for (const expected of exp.stdoutContains) {
|
|
309
|
+
if (!result.stdout.includes(expected)) {
|
|
310
|
+
failures.push(`Expected stdout to contain "${expected}"`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (exp.stderrContains) {
|
|
316
|
+
for (const expected of exp.stderrContains) {
|
|
317
|
+
if (!result.stderr.includes(expected)) {
|
|
318
|
+
failures.push(`Expected stderr to contain "${expected}"`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
results.push({
|
|
325
|
+
command,
|
|
326
|
+
result,
|
|
327
|
+
passed: failures.length === 0,
|
|
328
|
+
failures,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return results;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
336
|
+
// Debug Helpers
|
|
337
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Print a run result for debugging.
|
|
341
|
+
*/
|
|
342
|
+
export function debugResult(result: RunResult, label?: string): void {
|
|
343
|
+
console.log('\n' + '='.repeat(60));
|
|
344
|
+
if (label) {
|
|
345
|
+
console.log(`DEBUG: ${label}`);
|
|
346
|
+
console.log('-'.repeat(60));
|
|
347
|
+
}
|
|
348
|
+
console.log(`Exit Code: ${result.exitCode} (${result.success ? 'success' : 'failure'})`);
|
|
349
|
+
console.log(`Duration: ${result.duration}ms`);
|
|
350
|
+
console.log('\n--- STDOUT ---');
|
|
351
|
+
console.log(result.stdout || '(empty)');
|
|
352
|
+
console.log('\n--- STDERR ---');
|
|
353
|
+
console.log(result.stderr || '(empty)');
|
|
354
|
+
if (result.json) {
|
|
355
|
+
console.log('\n--- PARSED JSON ---');
|
|
356
|
+
console.log(JSON.stringify(result.json, null, 2));
|
|
357
|
+
}
|
|
358
|
+
console.log('='.repeat(60) + '\n');
|
|
359
|
+
}
|