import type { ForgeAdapter, Spec } from "@slowcook-ai/core"; import type { LlmClient } from "../refine/llm.js"; export declare const LABEL_TESTS_READY = "tests-ready"; export declare const LABEL_OVERRIDE_FREEZE = "override-freeze"; export declare const TESTS_INTEGRATION_DIR = "tests/integration"; export declare const TESTS_SCHEMA_DIR = "tests/schema"; export declare const MANIFESTS_DIR = ".brewing/manifests"; export declare const MOCK_HELPERS_DIR = "tests/helpers/mocks"; /** * Which artifacts testgen should emit for a given spec. Computed per-spec * based on what already exists on disk + whether the spec has `ui_behavior`: * * - `"full"` — neither handler tests nor UI tests exist; emit both (plus * any needed stubs + helpers). * - `"handler-only"` — spec has no `ui_behavior`; handler tests don't exist. * Emit handler test + handler stubs + helpers. This is 0.7.0 behavior. * - `"ui-only"` — handler tests already exist; spec has `ui_behavior`; UI * tests are missing. Emit ONLY UI test + UI stubs. Use-case: 0.7.5 adoption * on a brownfield story where the backend was built before UI tests existed. * * Mode is inferred by `collectTargetSpecs`; the LLM is told which mode it's * in via the user message so it emits only what's needed. */ export type TestgenMode = "full" | "handler-only" | "ui-only"; export interface TestgenContext { repoRoot: string; forge: ForgeAdapter; llm: LlmClient; model: string; cliVersion: string; baseBranch: string; /** When true, generate tests for every active spec that lacks them (CI path). */ all: boolean; /** When set, generate tests for this spec ID only (ops path / re-runs). */ specId: string | null; /** Idempotent base branch name. */ branchName: string; /** Injectable now for tests. */ now: Date; } export type TestgenOutcome = { kind: "tests-emitted"; storyIds: string[]; prUrl: string; prNumber: number; removedStoryIds: string[]; } | { kind: "nothing-to-generate"; reason: string; } | { kind: "pr-creation-blocked"; storyIds: string[]; branchName: string; }; /** * Main entry: for every spec in scope, if tests don't exist yet, generate them. * If the spec supersedes prior stories, remove the old tests + manifests for * those stories as part of the same PR (auto-applies `override-freeze` label, * citing the supersede chain in the PR body for auditability). */ export declare function runTestgen(ctx: TestgenContext): Promise; export interface TargetSpec { spec: Spec; mode: TestgenMode; } export declare function buildProjectContext(repoRoot: string): string; /** * Phase B2 (0.7.0) testgen output: one test file, zero-or-more route stubs, * zero-or-more mock helpers. The LLM emits these as XML-tagged blocks * (see TESTGEN_SYSTEM for the exact format); slowcook parses, de-duplicates * against existing files, and writes only what's new. */ export interface PageLinkHint { /** Page file the story's UI lives on (Next.js App Router). */ page: string; /** Component name the page must import + mount. */ component: string; /** Import specifier (e.g. "@/components/profile/ProfileEditForm"). */ importFrom: string; } export interface TestgenBundle { /** Handler test file content. Empty string when mode is `"ui-only"`. */ testContent: string; stubs: Array<{ path: string; contents: string; }>; helpers: Array<{ path: string; contents: string; }>; /** UI component test file content (tier-1 UI). Empty string when mode is `"handler-only"`. */ uiTestContent: string; /** UI component stubs — React/TSX files under src/components/ or src/app/**\/*.tsx. */ uiStubs: Array<{ path: string; contents: string; }>; /** * Optional page-link hint — when present, testgen templates a static * assertion that asserts the named page file imports AND mounts the * named component. Catches the "tests green but page doesn't render the * component" class of gap (rewo story-006, 2026-04-23). See * docs/plans/0.7.17-pipeline-gap-assertions.md §1. */ pageLink: PageLinkHint | null; } /** * Parse XML-tagged multi-artifact output into a TestgenBundle. * * Accepted shape (from the prompt): * ... — required unless mode="ui-only" * ... (zero or more) * ... (zero or more) * ... — required when mode="ui-only" or "full" with UI * ... (zero or more) * * Mode semantics (0.7.7+): * - `"handler-only"` — `` required; `` ignored if present. * - `"ui-only"` — `` required; `` ignored if present. * - `"full"` — both `` required AND `` required. * * Tolerant of code-fenced output: if the LLM wraps the whole thing in * ```, we strip it. If a block's contents are themselves code-fenced, * we strip those too — tier-1 test / helper / stub / UI files are raw TS/TSX. */ export declare function parseTestgenBundle(raw: string, storyId: string, mode?: TestgenMode): TestgenBundle; /** * Extract column names from DDL-shaped strings so * `buildSchemaAssertionTestContent` can assert each column ends up in * `supabase/migrations/`. Two matching paths: * * - **Explicit DDL phrasing** — matches regardless of other context: * - "Migration adds `profiles.handle_confirmed ...`" * - "adds `profiles.handle_changed_at timestamptz`" * - "alter table profiles add column handle text" * - "add column handle boolean" * * - **Table.column references near a migration keyword** — catches the * shape story-005 used where DDL intent lives in `preconditions`: * - "`profiles.handle` column exists, is unique, and is populated for * every profile (backfill migration part of this story)" * Guarded on a migration/backfill/alter keyword IN THE SAME STRING so * unrelated `profiles.handle` references in prose don't trigger. * * Returns unique column names (deduped, original order). * * Intentionally lax on TABLE name: rewo has exactly one mutated table per * story so far (`profiles`); if/when that stops holding, we can either * tighten the regex or include table names in the test assertions. */ export declare function extractDdlColumnsFromStrings(strings: string[]): string[]; /** * @deprecated since 0.7.18 — use `extractDdlColumnsFromSpec` which also * scans `preconditions`. Kept as a thin shim so older tests / callers * don't break at import time. */ export declare function extractDdlColumnsFromInvariants(invariants: string[]): string[]; /** * Scan every spec field that commonly carries DDL intent. `invariants` * covers the explicit "Migration adds..." shape (story-006); `preconditions` * covers the implicit "column exists, is unique, and is populated" shape * with the migration keyword (story-005). Acceptance scenarios can also * mention DDL ("Given a migration has been applied that adds...") so we * include those too. */ export declare function extractDdlColumnsFromSpec(spec: Spec): string[]; /** * Produce the `tests/schema/story-N.test.ts` file contents, or null when * the spec doesn't describe DDL (no invariants matched). The test reads * `supabase/migrations/*.sql` and asserts each column mentioned in the * invariants appears in some migration's `alter table ... add column ...` * statement. Shallow — doesn't check TYPE or NOT NULL — but catches the * "forgot to write the migration entirely" case that bit story-005/006. */ export declare function buildSchemaAssertionTestContent(spec: Spec): { path: string; contents: string; } | null; /** * 0.12.10 (slowcook#7) — code→schema presence check. Walks src/ for * literal `.from('').select('')` calls and asserts every * referenced column appears in some `CREATE TABLE` body or * `ALTER TABLE … ADD COLUMN` clause under `supabase/migrations/`. * Catches the gap class where brew tier-1 mocks the database away, so a * SELECT against a column with no migration ships green. * * Per-story emission (file name carries `story-N`) but the test logic * scans the WHOLE repo — the assertion is project-wide. Multiple stories * emit identical-content files; redundant but fits slowcook's tier-1 * frame. * * v1 limitations: * - Only literal table/column strings. Computed table names * (`from(tableName)` where tableName is a variable) are skipped. * - `select('*')` skipped — by definition matches everything. * - Relation sub-selects like `member:profiles!member_id(id, name)`: * the alias `member` is treated as a relation pointer and skipped * (we don't try to recursively validate the joined table here). * - Views and computed columns aren't tracked — unknown tables are * skipped (no false positive when a CTE / view is referenced). */ export declare function buildSchemaPresenceTestContent(spec: Spec): { path: string; contents: string; }; /** * Resolve an `@/…` import specifier back to a repo-relative source file. * Mirrors Next.js / tsconfig's default alias (`@/*` → `src/*`). Falls * back to the raw path if the specifier isn't prefixed. */ export declare function resolveImportToSourcePath(importFrom: string): string; /** * Produce `tests/integration/story-N-styling.test.ts` from a `` * hint. Asserts the component file has visible styling attention — * **presence** checks, not pixel-perfect visual regression: * * 1. At least 4 `className=` occurrences. Raw unstyled HTML has 0-1; * a real styled component has many. * 2. At least one Tailwind-shaped class name (`bg-`, `text-`, `border-`, * `rounded`, `px-`, etc.) so the component is using the project's * design-token family, not just scattered arbitrary classes. * * Doesn't couple tests to specific tokens (brew can choose `bg-coral`, * `bg-primary`, whatever the project uses). Catches the "zero-className * raw HTML" failure mode observed on rewo story-005/006 (2026-04-23, * two consecutive brews shipping unstyled components). See memory * `project_styling_gap_in_pipeline.md`. * * Static source-file scan — no render, no jsdom, no fixture required. * Same pattern as `buildPageLinkTestContent` / `buildSchemaAssertionTestContent`. */ export declare function buildStylingPresenceTestContent(spec: Spec, hint: PageLinkHint): { path: string; contents: string; }; /** * Produce the `tests/integration/story-N-page.test.ts` file contents from * a `` hint. Asserts both IMPORT (so the page file references * the component) and JSX USAGE (so it actually mounts it) — an unused * import is as broken as a missing one. */ export declare function buildPageLinkTestContent(spec: Spec, hint: PageLinkHint): { path: string; contents: string; }; /** * Tier-1 conformance lint. Run on every generated test file before commit. * Catches patterns the prompt forbids — inline `vi.mock`, `fetch(...)`, * HTTP-loopback mocking libraries, and skipped tests. These slip through * when the LLM reverts to habits from tier-0 (HTTP-loopback) examples. * * Returns an array of violations (file:line:pattern:reason); empty means * the file is conformant. Caller decides whether violations halt the run * or emit a warning — in 0.6.6 they halt, because a non-conformant tier-1 * test file defeats the whole point of the redesign. * * Sanitisation: string literals and comments are blanked before scanning * (same approach as extractTestIdsFromFile) so \`"uses vi.mock style"\` * in a JSDoc or message string doesn't trip the lint. */ export interface TierOneViolation { line: number; pattern: string; reason: string; } export declare function lintTierOneTest(filePath: string, source: string): TierOneViolation[]; export interface ImportClosureViolation { test: string; importPath: string; reason: "missing" | "directory-without-index"; } export declare function validateImportClosure(args: { repoRoot: string; testFilePath: string; testContent: string; emittedHelperPaths: string[]; emittedStubPaths?: string[]; }): ImportClosureViolation[]; export declare function formatImportClosureViolations(violations: ImportClosureViolation[]): string; /** * Parse the emitted TS file and pull out test IDs in the format * > > * matching what `vitest list` would emit. * * This is a lightweight text-level extraction — we parse for `describe(` and * `it(` invocations and their string literal arguments, tracking nesting. * Robust against typical usage; falls back to a single synthetic entry if * nothing recognisable is found. */ /** * Read an existing test file from disk and extract its test IDs. Thin * wrapper around extractTestIdsFromFile for the "ui-only" code path where * the handler test already exists and we need its IDs to preserve in the * rewritten combined manifest. */ export declare function extractTestIdsFromExistingFile(repoRoot: string, filePath: string): string[]; export declare function extractTestIdsFromFile(filePath: string, source: string): string[]; /** * sc#240 — up to two newest story-test files from the history index, * excerpted (first 60 lines each) as style exemplars for the prompt. */ export declare function mineTestExemplars(repoRoot: string): string | null; //# sourceMappingURL=agent.d.ts.map