Skip to content

Commit a9b39d9

Browse files
authored
Merge pull request #33 from tokenhost/feat/generate-with-tests-scaffold
Generate: add --with-tests test scaffold emission
2 parents 0d5b01b + 39a3e38 commit a9b39d9

6 files changed

Lines changed: 126 additions & 1 deletion

File tree

Readme.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ pnpm legacy:build-run
4040

4141
## Testing
4242

43+
Token Host Builder uses a two-layer quality model:
44+
45+
- Builder framework tests: validate schema/generator/CLI/runtime behavior.
46+
- Generated app tests: validate that produced apps behave correctly for their schema (canonical `job-board` is enforced in CI today).
47+
4348
Fast local suite (no local chain required):
4449

4550
```bash
@@ -53,6 +58,27 @@ Local integration suite (requires `anvil` on PATH):
5358
pnpm test:integration
5459
```
5560

61+
Generated app test scaffold (issue #28 slice):
62+
63+
```bash
64+
pnpm th generate apps/example/job-board.schema.json --out artifacts/job-board --with-tests
65+
cd artifacts/job-board/ui
66+
pnpm test
67+
```
68+
69+
Current integration coverage includes:
70+
71+
- preview auto-deploy behavior and manifest publication checks,
72+
- local faucet behavior checks,
73+
- canonical `apps/example/job-board.schema.json` end-to-end assertions:
74+
- Candidate CRUD flows,
75+
- JobPosting paid-create enforcement,
76+
- generated UI route health checks.
77+
78+
Planned expansion:
79+
80+
- generated apps emitted by `th generate` should include app-level test scaffolds/scripts so downstream repos can run schema-specific tests by default.
81+
5682
## CI
5783

5884
PRs run two required jobs:

packages/cli/src/index.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,24 @@ function copyDir(srcDir: string, destDir: string) {
108108
}
109109
}
110110

111+
function addGeneratedUiTestScaffold(uiDir: string, templateDir: string) {
112+
const scaffoldDir = path.join(templateDir, 'test-scaffold');
113+
if (!fs.existsSync(scaffoldDir)) {
114+
throw new Error(`Missing test scaffold template at ${scaffoldDir}`);
115+
}
116+
117+
copyDir(scaffoldDir, uiDir);
118+
119+
const packageJsonPath = path.join(uiDir, 'package.json');
120+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
121+
const scripts = { ...(pkg.scripts || {}) };
122+
scripts.test = scripts.test || 'pnpm run test:contract && pnpm run test:ui';
123+
scripts['test:contract'] = scripts['test:contract'] || 'node tests/contract/smoke.mjs';
124+
scripts['test:ui'] = scripts['test:ui'] || 'node tests/ui/smoke.mjs';
125+
pkg.scripts = scripts;
126+
fs.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2) + '\n');
127+
}
128+
111129
function publishManifestToUiSite(uiSiteDir: string, manifestJson: string) {
112130
ensureDir(uiSiteDir);
113131
ensureDir(path.join(uiSiteDir, '.well-known', 'tokenhost'));
@@ -1438,7 +1456,8 @@ program
14381456
.argument('<schema>', 'Path to THS schema JSON file')
14391457
.option('--out <dir>', 'Output directory', 'artifacts')
14401458
.option('--no-ui', 'Do not generate UI output')
1441-
.action((schemaPath: string, opts: { out: string; ui: boolean }) => {
1459+
.option('--with-tests', 'Emit generated app test scaffold', false)
1460+
.action((schemaPath: string, opts: { out: string; ui: boolean; withTests: boolean }) => {
14421461
const input = readJsonFile(schemaPath);
14431462
const structural = validateThsStructural(input);
14441463
if (!structural.ok) {
@@ -1475,6 +1494,11 @@ program
14751494
ensureDir(path.dirname(thsTsPath));
14761495
fs.writeFileSync(thsTsPath, renderThsTs(schema));
14771496

1497+
if (opts.withTests) {
1498+
addGeneratedUiTestScaffold(uiDir, templateDir);
1499+
console.log(`Wrote ui/tests/ (generated app test scaffold)`);
1500+
}
1501+
14781502
console.log(`Wrote ui/ (Next.js static export template)`);
14791503
}
14801504

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Generated app test scaffold
2+
3+
This directory is emitted by `th generate --with-tests`.
4+
5+
- `contract/smoke.mjs` validates baseline generated app contract test preconditions.
6+
- `ui/smoke.mjs` validates baseline generated UI route/component preconditions.
7+
8+
These are starter tests and are intended to be expanded with schema-specific assertions.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import assert from 'node:assert/strict';
2+
import fs from 'node:fs';
3+
import path from 'node:path';
4+
5+
function mustExist(root, relPath) {
6+
const p = path.join(root, relPath);
7+
assert.equal(fs.existsSync(p), true, `Missing required generated file: ${relPath}`);
8+
return p;
9+
}
10+
11+
const root = process.cwd();
12+
const thsPath = mustExist(root, 'src/generated/ths.ts');
13+
mustExist(root, 'src/lib/app.ts');
14+
mustExist(root, 'src/lib/abi.ts');
15+
16+
const thsSource = fs.readFileSync(thsPath, 'utf-8');
17+
assert.match(thsSource, /export const ths = /, 'Generated THS export is missing.');
18+
19+
console.log('PASS contract smoke scaffold');
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import assert from 'node:assert/strict';
2+
import fs from 'node:fs';
3+
import path from 'node:path';
4+
5+
function mustExist(root, relPath) {
6+
const p = path.join(root, relPath);
7+
assert.equal(fs.existsSync(p), true, `Missing required generated UI file: ${relPath}`);
8+
}
9+
10+
const root = process.cwd();
11+
12+
for (const relPath of [
13+
'app/layout.tsx',
14+
'app/page.tsx',
15+
'app/[collection]/layout.tsx',
16+
'app/[collection]/page.tsx',
17+
'app/[collection]/new/page.tsx'
18+
]) {
19+
mustExist(root, relPath);
20+
}
21+
22+
console.log('PASS ui smoke scaffold');

test/testCliGenerateUi.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ describe('th generate (UI template)', function () {
6868

6969
expect(fs.existsSync(path.join(outDir, 'ui', 'package.json'))).to.equal(true);
7070
expect(fs.existsSync(path.join(outDir, 'ui', 'app', 'page.tsx'))).to.equal(true);
71+
expect(fs.existsSync(path.join(outDir, 'ui', 'tests'))).to.equal(false);
7172

7273
const generatedThs = fs.readFileSync(path.join(outDir, 'ui', 'src', 'generated', 'ths.ts'), 'utf-8');
7374
expect(generatedThs).to.include('export const ths =');
@@ -110,4 +111,29 @@ describe('th generate (UI template)', function () {
110111
expect(fs.existsSync(path.join(outDir, 'contracts', 'App.sol'))).to.equal(true);
111112
expect(fs.existsSync(path.join(outDir, 'ui'))).to.equal(false);
112113
});
114+
115+
it('emits generated app test scaffold with --with-tests', function () {
116+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'th-ui-gen-tests-'));
117+
const schemaPath = path.join(dir, 'schema.json');
118+
const outDir = path.join(dir, 'out');
119+
writeJson(schemaPath, minimalSchema());
120+
121+
const res = runTh(['generate', schemaPath, '--out', outDir, '--with-tests'], process.cwd());
122+
expect(res.status, res.stderr || res.stdout).to.equal(0);
123+
124+
const uiDir = path.join(outDir, 'ui');
125+
expect(fs.existsSync(path.join(uiDir, 'tests', 'contract', 'smoke.mjs'))).to.equal(true);
126+
expect(fs.existsSync(path.join(uiDir, 'tests', 'ui', 'smoke.mjs'))).to.equal(true);
127+
128+
const pkg = JSON.parse(fs.readFileSync(path.join(uiDir, 'package.json'), 'utf-8'));
129+
expect(pkg?.scripts?.test).to.equal('pnpm run test:contract && pnpm run test:ui');
130+
expect(pkg?.scripts?.['test:contract']).to.equal('node tests/contract/smoke.mjs');
131+
expect(pkg?.scripts?.['test:ui']).to.equal('node tests/ui/smoke.mjs');
132+
133+
const contractSmoke = runCmd('node', ['tests/contract/smoke.mjs'], uiDir);
134+
expect(contractSmoke.status, contractSmoke.stderr || contractSmoke.stdout).to.equal(0);
135+
136+
const uiSmoke = runCmd('node', ['tests/ui/smoke.mjs'], uiDir);
137+
expect(uiSmoke.status, uiSmoke.stderr || uiSmoke.stdout).to.equal(0);
138+
});
113139
});

0 commit comments

Comments
 (0)