Skip to content

feat(intent): AppTestIntentGenerator emits <name>.test + graduate the runner to @aerokit/test#6289

Merged
delchev merged 1 commit into
masterfrom
feat/aerokit-test-apptest-generator
Jul 13, 2026
Merged

feat(intent): AppTestIntentGenerator emits <name>.test + graduate the runner to @aerokit/test#6289
delchev merged 1 commit into
masterfrom
feat/aerokit-test-apptest-generator

Conversation

@delchev

@delchev delchev commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

What

Phase 3 of the generated-app integration-test feature (PROPOSAL_APPTEST.md, piloted in KeyFolders as @keyfolders/kf-test + a hand-written kf-mod-countries.test). Nothing today verifies that a generated app actually does what its .intent says; this lands the upstream generator + graduates the runner.

Pieces

1. npm/test = @aerokit/test (the runner). The generic Playwright runner graduated from the pilot. It reads a module's <name>.test manifest (WHAT the app promises) and knows HOW to verify each promise against the generated Harmonia UI + reused REST controllers — flows: list, crud, rest (same contract, no browser), multilingual, and opt-in shell. Test records carry an APPTEST- prefix and are cleaned up; seed data is never mutated. This is the app-integration surface (browser + REST from outside), distinct from the in-instance @aerokit/sdk/junit unit-test facade.

2. AppTestIntentGenerator (@Order(900), engine-intent). Emits one <name>.test JSON manifest per module. It runs after EdmIntentGenerator (200) and reads the .model back for the resolved per-entity metadata (perspective → sanitized REST path, dataName → table, menuLabel → plural, layout type, nav group, multilingual) — the same source the Harmonia templates consume, so paths never drift — while taking the logical field/relation data (type/required/unique/length/major, dropdown relations) straight from the intent. Composition detail children and cross-model projections are excluded (no standalone list); the multilingual sample is derived from inline base + language: seeds when available.

3. .test registered as an intent-owned, scrub-managed extension. Regenerated on every Generate, never hand-edited (a free extension — JS *.test.js files carry extension js, no clash). The per-module test/ harness at the repo root references it but does not own it.

Manifest shape (generated)

{
  "module": "orders",
  "standaloneShell": "/services/web/<project>/gen/orders/index.html",
  "restBase": "/services/java/<project>/gen/orders/api",
  "idProperty": "Id",
  "languages": ["en", "bg"],
  "entities": [ { "name": "Customer", "label": "Customer", "labelPlural": "Customers",
    "layout": "manage-list", "route": "#/Customer", "navGroup": "...",
    "api": "/.../CustomerController", "table": "...", "fields": [...], "relations": [...] } ]
}

Tests

  • AppTestIntentGeneratorTest — 5 unit tests over the pure manifest builder (module coordinates, entity metadata + fields, multilingual sample from seeds, dropdown relations, detail/projection exclusion).
  • IntentEngineIT — the full parse → generate pipeline now also asserts orders.test is written with the sanitized REST base, the Order document layout, the Customer manage-list + controller/route, the multilingual Country, and the excluded OrderItem composition detail.
  • mvn formatter:validate clean on both touched modules.

Follow-ups (Phase 4, per PROPOSAL_APPTEST.md / kf-catalog BACKLOG)

The kf-infra module-test CI workflow and rolling the 4-file test/ harness across modules (mirroring the countries pilot) land next; document/roll-up/calendar/report runner flows (Phase 2) extend the manifest schema + runner beyond the list/crud/rest/multilingual set.

🤖 Generated with Claude Code

… runner to @aerokit/test

Phase 3 of the generated-app integration-test feature (PROPOSAL_APPTEST.md):

- npm/test = @aerokit/test — the generic Playwright runner graduated from the
  KeyFolders pilot (@keyfolders/kf-test). Reads a module's <name>.test manifest
  and drives list/crud/rest/multilingual/(opt-in)shell flows against a running
  instance. Distinct from the in-instance @aerokit/sdk/junit unit-test surface.
- AppTestIntentGenerator (@order(900), engine-intent) emits one <name>.test JSON
  manifest per module — module id, standalone shell + sanitized REST base, id
  property, languages, and per entity: label/plural/layout/route/nav-group/api/
  table (read back from the .model the EDM generator wrote at @order(200), the
  same source the Harmonia templates consume) + fields/relations (logical data
  from the intent). Composition detail children and cross-model projections are
  excluded; the multilingual sample is derived from inline seeds.
- '.test' registered as an intent-owned (scrub-managed) extension — it is
  regenerated every Generate, never hand-edited (a free extension: JS *.test.js
  carry extension 'js').

Tests: AppTestIntentGeneratorTest (5, pure manifest builder) + an IntentEngineIT
assertion (the full pipeline emits orders.test with the right coordinates, the
Order document layout, Customer manage-list, the multilingual Country, and the
excluded OrderItem detail). formatter:validate clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
await cfg.beforeCreate?.(page, record);
await fillForm(page, manifest, entity, record, relationSamples, opts);
await page.getByRole('button', { name: 'Create' }).click();
await expect(page).toHaveURL(new RegExp(entity.route.replace(/[#/]/g, '\\$&') + '$'));
await expect(page).toHaveURL(/\/edit$/);
await fillField(page, handle, updated, opts);
await page.getByRole('button', { name: 'Save' }).click();
await expect(page).toHaveURL(new RegExp(entity.route.replace(/[#/]/g, '\\$&') + '$'));
Comment thread npm/test/src/index.js
const manifest = typeof manifestRef === 'string' ? JSON.parse(fs.readFileSync(manifestRef, 'utf8')) : manifestRef;
for (const entity of manifest.entities ?? []) {
test.describe(`${manifest.module} / ${entity.name}`, () => {
listFlow(manifest, entity, opts);
Comment thread npm/test/src/index.js
listFlow(manifest, entity, opts);
crudFlow(manifest, entity, opts);
restFlow(manifest, entity, opts);
multilingualFlow(manifest, entity, opts);
Comment thread npm/test/src/index.js
crudFlow(manifest, entity, opts);
restFlow(manifest, entity, opts);
multilingualFlow(manifest, entity, opts);
shellFlow(manifest, entity, opts);
String baseName = IntentNaming.baseName(context);
Map<String, Map<String, Object>> edmEntities = readModelEntities(context, baseName);
if (edmEntities.isEmpty()) {
LOGGER.debug("Skipping app-test manifest for [{}] — no .model entities to describe", baseName);

Map<String, Object> manifest = buildManifest(baseName, context.getProjectName(), model, edmEntities);
context.writeModelFile(baseName + ".test", GSON.toJson(manifest) + "\n");
LOGGER.debug("Generated app-test manifest [{}.test]", baseName);
}
}
} catch (RuntimeException e) {
LOGGER.warn("Failed to read the .model for the app-test manifest of [{}]; skipping", baseName, e);
@delchev
delchev merged commit aa943f1 into master Jul 13, 2026
9 of 10 checks passed
@delchev
delchev deleted the feat/aerokit-test-apptest-generator branch July 13, 2026 16:39
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.

2 participants