Skip to content

feat(test): add "test scaffold" to emit a schema-correct starter plan or backend skeleton#180

Merged
zeshi-du merged 1 commit into
TestSprite:mainfrom
Andy00L:feat/test-scaffold
Jul 6, 2026
Merged

feat(test): add "test scaffold" to emit a schema-correct starter plan or backend skeleton#180
zeshi-du merged 1 commit into
TestSprite:mainfrom
Andy00L:feat/test-scaffold

Conversation

@Andy00L

@Andy00L Andy00L commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds testsprite test scaffold — a first test today starts from hand-copied
JSON, and any shape mistake only surfaces as a paid create round-trip. This
emits a schema-correct starter: by default a frontend plan (the exact
CliPlanInput shape --plan-from ingests, derived from the source-of-truth
types, not docs), or with --type backend the minimal Python requests
skeleton the onboarding skill mandates (a status assertion, and the test
function is CALLED so it can never pass vacuously). Pure-local: no network,
no credentials. --out <path> writes a file (refusing to clobber without
--force); stdout otherwise, so scaffold > plan.json works. The projectId
is pre-filled from TESTSPRITE_PROJECT_ID when present, else a placeholder
that names the command to run.

Related issue

Fixes #101

Type of change

  • New feature (non-breaking change that adds functionality)

Checklist

  • PR targets the main branch.
  • Commits follow Conventional Commits.
  • npm run lint and npm run format:check pass.
  • npm run typecheck passes.
  • npm test passes and coverage stays at or above the 80% gate.
  • New behavior is covered by unit tests (mock-based; no network).
  • No secrets, API keys, internal endpoints, or personal data are included.

Notes for reviewers

The emitted step types come from the real PLAN_STEP_TYPES enum via the
CliPlanInput type (a drift in the enum would fail compilation and the
round-trip test). No overlap with init/setup (configure + skill install)
per the triage note. File writes reuse the existing openOutputFile guards.

Summary by CodeRabbit

  • New Features

    • Added a new test scaffold CLI subcommand to generate starter test scaffolds.
    • Supports both frontend and backend templates, outputting to stdout (text) or JSON, including --output json.
    • Added --type, --out, and --force to control scaffold type and where the generated plan.json is written.
  • Bug Fixes

    • Prevents overwriting existing files by default; overwrite requires --force.
    • Rejects --out when the target path is an existing directory unless --force is provided.
  • Tests

    • Added end-to-end coverage for scaffold stdout structure and --out/overwrite behavior.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5a17a9c2-8089-43de-b87b-64fc81aaa11e

📥 Commits

Reviewing files that changed from the base of the PR and between 6121944 and 925bab4.

⛔ Files ignored due to path filters (1)
  • test/__snapshots__/help.snapshot.test.ts.snap is excluded by !**/*.snap, !**/*.snap
📒 Files selected for processing (2)
  • src/commands/test.test.ts
  • src/commands/test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/commands/test.ts
  • src/commands/test.test.ts

Walkthrough

Adds a new test scaffold CLI command that generates either a frontend CliPlanInput starter or a backend Python scaffold, with optional --out file writing and --force overwrite handling. Includes CLI wiring and tests for the new outputs and file semantics.

Changes

Test Scaffold Feature

Layer / File(s) Summary
Scaffold types and runScaffold implementation
src/commands/test.ts
Adds ScaffoldOptions and CliBackendScaffold exports and implements runScaffold, which builds frontend or backend scaffold output, prints it, or writes it through the existing output helpers with overwrite checks; also reformats the fs import.
Commander subcommand wiring
src/commands/test.ts
Wires test scaffold into createTestCommand with --type, --out, and --force flags routed to runScaffold.
Scaffold tests
src/commands/test.test.ts
Adds runScaffold import, updates the command surface assertion to include scaffold, and adds tests for frontend and backend generation, projectId defaulting and prefill, and --out write and overwrite behavior.

Estimated code review effort: 2 (Simple) | ~15 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant CLI as test scaffold CLI
  participant runScaffold
  participant FileSystem

  User->>CLI: testsprite test scaffold --type frontend|backend [--out path] [--force]
  CLI->>runScaffold: call with ScaffoldOptions
  runScaffold->>runScaffold: build CliPlanInput or CliBackendScaffold
  alt --out provided
    runScaffold->>FileSystem: check existsSync and directory guard
    alt exists and no --force
      runScaffold-->>CLI: refuse overwrite
    else
      runScaffold->>FileSystem: write scaffold file
      runScaffold-->>CLI: return written output
    end
  else no --out
    runScaffold-->>CLI: print scaffold JSON to stdout
  end
  CLI-->>User: output result
Loading

Related Issues: #101

Suggested reviewers: interferon0

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately describes the new scaffold command and its frontend/backend scaffold output.
Linked Issues check ✅ Passed The summary covers the requested scaffold command, frontend plan, backend skeleton, stdout/--out handling, and overwrite protection.
Out of Scope Changes check ✅ Passed Changes appear limited to the scaffold command and its tests, with no unrelated functionality added.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@zeshi-du

zeshi-du commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Approved — no changes requested on substance. This went CONFLICTING purely because today's merge wave landed a large batch into the same files first. Rebase onto current main (and re-run npm test if the help snapshot moved), push, and this merges.

@Andy00L Andy00L force-pushed the feat/test-scaffold branch from 6c806d0 to 6121944 Compare July 5, 2026 20:17

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/commands/test.ts`:
- Around line 3877-3971: The `runScaffold` backend branch writes raw Python to
`--out` even when `opts.output` is `json`, so the file misses the documented
JSON envelope. Update the `runScaffold` path that builds `payload` and `body` so
the `openOutputFile`/`fileOut.writeChunk` branch writes `JSON.stringify(payload,
null, 2)` (or equivalent JSON output) whenever `opts.output === 'json'`, while
preserving the current raw code body for non-JSON output.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8018b84e-48a3-45f5-9bb8-ffb25e03b193

📥 Commits

Reviewing files that changed from the base of the PR and between 6c806d0 and 6121944.

⛔ Files ignored due to path filters (1)
  • test/__snapshots__/help.snapshot.test.ts.snap is excluded by !**/*.snap, !**/*.snap
📒 Files selected for processing (2)
  • src/commands/test.test.ts
  • src/commands/test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/commands/test.test.ts

Comment thread src/commands/test.ts
Comment on lines +3877 to +3971
/** JSON payload `test scaffold --type backend` prints under --output json. */
export interface CliBackendScaffold {
type: 'backend';
language: 'python';
code: string;
}

/**
* `test scaffold` — emit a schema-correct starter test definition so a first
* test never starts from hand-copied JSON. Pure-local: no network, no
* credentials, no filesystem reads. The frontend template is a `CliPlanInput`
* (the exact shape `--plan-from` ingests; sourceRef: CliPlanInput /
* PLAN_STEP_TYPES above), so `scaffold | create --plan-from -`-style flows
* validate out of the box. The backend template is the minimal `requests`
* script the onboarding skill mandates: define a test function with a
* concrete status assertion, then CALL it (a defined-but-never-called test
* would pass without asserting anything).
*/
export async function runScaffold(
opts: ScaffoldOptions,
deps: TestDeps = {},
): Promise<CliPlanInput | CliBackendScaffold> {
const out = makeOutput(opts.output, deps);
const stderrFn = deps.stderr ?? ((line: string) => process.stderr.write(`${line}\n`));
const env = deps.env ?? process.env;
// Pre-fill the project id from TESTSPRITE_PROJECT_ID when the caller's
// environment carries one; otherwise a clearly-marked placeholder the user
// swaps after running `testsprite project list`.
const projectId =
typeof env.TESTSPRITE_PROJECT_ID === 'string' && env.TESTSPRITE_PROJECT_ID.length > 0
? env.TESTSPRITE_PROJECT_ID
: '<run: testsprite project list>';

let payload: CliPlanInput | CliBackendScaffold;
let body: string;
if (opts.scaffoldType === 'frontend') {
const plan: CliPlanInput = {
projectId,
type: 'frontend',
name: 'My first frontend test',
description: 'Replace with one sentence describing what this test verifies.',
priority: 'p2',
planSteps: [
{
type: 'action',
description: 'Navigate to /login and sign in with a seeded test account',
},
{ type: 'action', description: 'Open the first product page and click "Add to cart"' },
{ type: 'assertion', description: 'Assert that the cart badge shows 1 item' },
],
};
payload = plan;
body = `${JSON.stringify(plan, null, 2)}\n`;
} else {
const code = [
'import requests',
'',
'# Replace with your API base URL (must be reachable from the internet).',
'BASE_URL = "https://staging.example.com"',
'',
'',
'def test_health_endpoint() -> None:',
' response = requests.get(f"{BASE_URL}/health", timeout=30)',
' assert response.status_code == 200, f"expected 200, got {response.status_code}"',
'',
'',
'# The test function MUST be called: TestSprite executes this file top to',
'# bottom, so a defined-but-never-called function would pass vacuously.',
'test_health_endpoint()',
'',
].join('\n');
payload = { type: 'backend', language: 'python', code };
body = code;
}

if (opts.out !== undefined) {
const resolved = isAbsolute(opts.out) ? opts.out : resolve(process.cwd(), opts.out);
// Never clobber silently: scaffolds are starting points the user edits, so
// an accidental re-run must not erase their work. --force opts in.
if (!opts.force && existsSync(resolved)) {
throw localValidationError('out', `already exists: ${resolved}. Pass --force to overwrite`);
}
const sink = openOutputFile(opts.out); // reuses the directory/parent guards
const fileOut = makeFileOutput(opts.output, sink);
await fileOut.writeChunk(body);
await closeOutputFile(sink, true);
stderrFn(`Scaffold written to ${resolved}`);
return payload;
}

// No --out: the scaffold body IS the stdout payload (`> plan.json` works).
out.print(payload, () => body.trimEnd());
return payload;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect makeFileOutput to see if it already serializes based on output mode
ast-grep run --pattern 'function makeFileOutput($_, $_) { $$$ }' --lang typescript src/lib/output.ts
rg -n -A 20 'function makeFileOutput' src/lib/output.ts

Repository: TestSprite/testsprite-cli

Length of output: 163


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files 'src/**' | rg '(^|/)(output|test)\.(ts|js|tsx|jsx)$|src/commands/test\.ts|src/lib/dry-run/samples\.ts|src/lib/output\.ts'

echo
echo "== outlines =="
ast-grep outline src/lib/output.ts --view expanded || true
ast-grep outline src/commands/test.ts --view expanded || true

echo
echo "== locate file-output helpers =="
rg -n -A 40 -B 10 'function (makeFileOutput|openOutputFile|closeOutputFile)|const (makeFileOutput|openOutputFile|closeOutputFile)|class FileSink|interface FileSink' src/lib/output.ts src/commands/test.ts

echo
echo "== scaffold branch in test.ts =="
rg -n -A 80 -B 20 'runScaffold|scaffold written|makeFileOutput|openOutputFile|closeOutputFile|out.print\(' src/commands/test.ts

Repository: TestSprite/testsprite-cli

Length of output: 50382


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== output.ts: Output class =="
sed -n '69,130p' src/lib/output.ts | cat -n

echo
echo "== test.ts: runScaffold =="
sed -n '3895,3970p' src/commands/test.ts | cat -n

echo
echo "== any tests for scaffold output =="
rg -n -A 6 -B 6 'runScaffold|CliBackendScaffold|test scaffold|Scaffold written|makeFileOutput' src --glob '*test*' --glob '*spec*'

Repository: TestSprite/testsprite-cli

Length of output: 19925


--out should write the JSON envelope in --output json mode src/commands/test.ts:3959-3961
fileOut.writeChunk(body) always writes raw Python to the target, so testsprite test scaffold --type backend --output json --out foo skips the documented JSON envelope. Switch the file contents to payload/JSON.stringify(...) when opts.output === 'json'.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/test.ts` around lines 3877 - 3971, The `runScaffold` backend
branch writes raw Python to `--out` even when `opts.output` is `json`, so the
file misses the documented JSON envelope. Update the `runScaffold` path that
builds `payload` and `body` so the `openOutputFile`/`fileOut.writeChunk` branch
writes `JSON.stringify(payload, null, 2)` (or equivalent JSON output) whenever
`opts.output === 'json'`, while preserving the current raw code body for
non-JSON output.

Source: Path instructions

@zeshi-du

zeshi-du commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

One more rebase, sorry — your own stack collided with itself: #132 and #176 merged first this round, moving the same test.ts anchors and the help snapshot again. Still approved, nothing else requested. (Tip for the remaining ones: rebasing them all onto main in one local session and pushing together avoids another cascade.)

@Andy00L Andy00L force-pushed the feat/test-scaffold branch from 6121944 to 925bab4 Compare July 5, 2026 22:56
@Andy00L

Andy00L commented Jul 5, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto current main. Ready to merge.

@zeshi-du zeshi-du left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed: scoped, tests included, CI green across the matrix.

@zeshi-du zeshi-du merged commit 3305dfa into TestSprite:main Jul 6, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Hackathon] Add "testsprite test scaffold" to emit a schema-correct starter plan

2 participants