diff --git a/bdd/README.md b/bdd/README.md new file mode 100644 index 000000000..c35d10ab9 --- /dev/null +++ b/bdd/README.md @@ -0,0 +1,119 @@ +# BDD / Gherkin pilot + +A small, self-contained pilot that runs **Gherkin `.feature` specs on the +project's existing Vitest runner** — no second test framework, no extra CI lane. + +The goal is to evaluate whether describing system behaviour in human-readable +`Given / When / Then` is worth adopting more widely, **before** committing to it. + +``` +bdd/ +├── features/ # the human-readable specs (.feature) +│ ├── entity-predicate.feature # pure smoke spec — always runnable +│ └── ka-lifecycle.feature # @devnet — the real KA publish lifecycle +├── steps/ # step definitions that bind specs to code +│ ├── entity-predicate.steps.ts +│ └── ka-lifecycle.steps.ts +├── support/ +│ └── world.ts # shared devnet connection + HTTP helpers +├── vitest.config.ts +└── package.json +``` + +## Why this approach + +The binding is [`@amiceli/vitest-cucumber`](https://www.npmjs.com/package/@amiceli/vitest-cucumber). +It was chosen deliberately over standalone Cucumber: + +- **One runner, one CI lane.** `describeFeature/Scenario/Given/When/Then` + compile down to Vitest's own `describe/test`. Its peer dependency is + `vitest ^4.0.4`, which the repo's `^4.0.18` satisfies — no runner duplication, + no Jest typings, nothing new for CI to learn. +- **Real `.feature` files.** `loadFeature('./x.feature')` parses actual Gherkin + (Scenario Outline, Examples, Background, tags, hooks) — the specs are not + inline strings, so non-engineers can read and edit them. +- **Maintained and Vitest-4 native** (unlike `jest-cucumber`, which is Jest-first, + or `@deepracticex/vitest-cucumber`, which doesn't declare Vitest 4 support). + +It is **strict by design**: every step in a `.feature` must have a matching step +definition or the suite fails. That keeps specs and code honest. + +## The two specs + +### 1. `entity-predicate.feature` — the smoke spec (always runnable) + +Pins the OT-RFC-43/44 entity-predicate rename invariant +(`dkg:rootEntity → dkg:entity`, `dkg:assertionRootEntity → dkg:assertionEntity`): +during the dual-write migration window, a mixed-fleet node **must recognise both +the new and the legacy predicate** or it silently drops entity members. + +It exercises the real helpers in +[`packages/core/src/entity-predicate.ts`](../packages/core/src/entity-predicate.ts) +via a data-driven `Scenario Outline`. They are imported through the public +`@origintrail-official/dkg-core` export (declared as a workspace dependency), so +Turbo treats `bdd` as depending on `dkg-core` and **invalidates the smoke-spec +cache when that code changes** — the regression check stays honest. No devnet, +no network — it runs in milliseconds and proves the Gherkin→Vitest binding works +end to end. + +This is the package's default `test` script, so `turbo test` (the standard CI +command) runs it automatically — no extra CI lane: + +```bash +pnpm --filter @origintrail-official/dkg-bdd test +``` + +### 2. `ka-lifecycle.feature` — the real e2e (tagged `@devnet`) + +The canonical V10 flow expressed as behaviour: a draft assertion goes +`create → write → finalize → promote → publish` and ends as a confirmed +on-chain Knowledge Asset. The step definitions call the same daemon routes as +`devnet/v10-core-flows`' `fullPublish()`, sharing the produced +assertion/`kaId` through the test World. + +It is **tag-gated**: if no devnet is reachable it is skipped cleanly (rather than +failing), mirroring how the existing devnet suites guard. Availability is decided +by a **live `/api/status` probe**, not just on-disk provisioning — so a +stopped-but-provisioned cluster also skips instead of erroring. Like the other +`devnet/*` suites it is intentionally kept out of the default `turbo test` lane +and run explicitly: + +```bash +./scripts/devnet.sh clean && ./scripts/devnet.sh start 6 +node devnet/_bootstrap/bootstrap.cjs +pnpm --filter @origintrail-official/dkg-bdd test:devnet +``` + +To run both specs at once (the devnet one self-skips without a cluster): +`pnpm --filter @origintrail-official/dkg-bdd test:all`. + +## How to add a new spec + +1. Write `features/my-thing.feature` in plain Gherkin. +2. Create `steps/my-thing.steps.ts`: `loadFeature(...)` + `describeFeature(...)`, + implementing one step definition per line in the feature. +3. Reuse `support/world.ts` for any devnet/HTTP plumbing. +4. Tag devnet-dependent scenarios `@devnet` so they self-skip without a cluster. + +## Honest tradeoffs + +- **Where it pays off:** multi-step, spec-driven flows with stakeholders who read + them (KA lifecycle, conviction staking tiers, provenance modes, the KA-routes + parity/must-not-regress invariants). The `.feature` doubles as living spec. +- **Where it does not:** simple unit assertions, and anything where the cost is + the *infrastructure* (booting a 6-node devnet) rather than the assertion syntax + — Gherkin does nothing for that hard part. Don't rewrite the 9 existing devnet + scenarios; add Gherkin where the readable-spec value is real. +- The strict matching means every feature line needs an implementation — good for + rigor, but it is real maintenance. + +## Pilot status (branch `test/bdd-gherkin-pilot`) + +- **Smoke spec** — verified green locally: `9` Example rows / `27` step-tests + pass in ~8ms. It is the package's `test` script, so it runs under the standard + `turbo test` CI lane. +- **`@devnet` spec** — verified to skip cleanly (exit 0) when no cluster is + present. Every HTTP route, request payload and response field it uses has been + cross-checked against the live daemon handlers (`assertion.ts`, `memory.ts`, + `status.ts`) and matches `devnet/v10-core-flows`' `fullPublish()` — but it has + not been executed against a live cluster as part of this pilot. diff --git a/bdd/features/entity-predicate.feature b/bdd/features/entity-predicate.feature new file mode 100644 index 000000000..5992d7bd1 --- /dev/null +++ b/bdd/features/entity-predicate.feature @@ -0,0 +1,35 @@ +Feature: Knowledge Asset entity-member predicate recognition + # Behaviour spec for the OT-RFC-43 §10.1 / OT-RFC-44 §4 predicate rename: + # dkg:rootEntity -> dkg:entity + # dkg:assertionRootEntity -> dkg:assertionEntity + # During the dual-write migration window a mixed-fleet node MUST recognise + # BOTH the new and the legacy predicate, or entity members get silently + # dropped. This spec pins that behaviour in human-readable terms. + # + # This is the "smoke" spec: pure, deterministic, no devnet required. + + Background: + Given the entity-predicate migration helpers + + Scenario Outline: A KA entity-member predicate is recognised across the rename + When I check whether "" is a KA entity predicate + Then the recognition result is "" + + Examples: + | iri | expected | + | http://dkg.io/ontology/entity | true | + | http://dkg.io/ontology/rootEntity | true | + | http://dkg.io/ontology/assertionEntity | false | + | http://dkg.io/ontology/unrelated | false | + | http://schema.org/name | false | + + Scenario Outline: An assertion-seal entity predicate is recognised across the rename + When I check whether "" is an assertion-seal entity predicate + Then the recognition result is "" + + Examples: + | iri | expected | + | http://dkg.io/ontology/assertionEntity | true | + | http://dkg.io/ontology/assertionRootEntity | true | + | http://dkg.io/ontology/entity | false | + | http://dkg.io/ontology/rootEntity | false | diff --git a/bdd/features/ka-lifecycle.feature b/bdd/features/ka-lifecycle.feature new file mode 100644 index 000000000..bdf34597e --- /dev/null +++ b/bdd/features/ka-lifecycle.feature @@ -0,0 +1,30 @@ +@devnet +Feature: Knowledge Asset publish lifecycle (Working Memory -> Shared WM -> Verified) + # The canonical V10 flow a DKG agent performs: take a draft assertion through + # create -> write -> finalize -> promote -> publish, ending as a verifiable + # on-chain Knowledge Asset. Mirrors devnet/v10-core-flows fullPublish(), but + # expressed as behaviour anyone can read. + # + # Tagged @devnet: skipped automatically unless a local devnet is running + # ./scripts/devnet.sh clean && ./scripts/devnet.sh start 6 + # node devnet/_bootstrap/bootstrap.cjs + + Background: + Given a reachable devnet node with an authenticated agent + And the "devnet-test" context graph + + # Tag is repeated on the scenario (not only the feature): @amiceli/vitest-cucumber + # filters per-scenario, so the gate that skips this without a devnet lives here. + @devnet + Scenario: A signed assertion becomes a confirmed on-chain Knowledge Asset + Given a fresh draft assertion in the context graph + When I write 2 entity quads to the assertion + And I finalize the assertion + Then the assertion has a merkle root and an EIP-712 digest + + When I promote the assertion to shared working memory + Then at least 1 share is created + + When I publish the assertion to the chain + Then a knowledge asset id is returned + And the knowledge asset status is one of "confirmed,tentative,finalized" diff --git a/bdd/package.json b/bdd/package.json new file mode 100644 index 000000000..1ba43ad0f --- /dev/null +++ b/bdd/package.json @@ -0,0 +1,19 @@ +{ + "name": "@origintrail-official/dkg-bdd", + "version": "0.0.0", + "private": true, + "type": "module", + "description": "Gherkin/BDD test pilot — human-readable .feature specs running on the existing Vitest runner.", + "scripts": { + "test": "vitest run --config vitest.config.ts steps/entity-predicate.steps.ts", + "test:devnet": "vitest run --config vitest.config.ts steps/ka-lifecycle.steps.ts", + "test:all": "vitest run --config vitest.config.ts" + }, + "dependencies": { + "@origintrail-official/dkg-core": "workspace:*" + }, + "devDependencies": { + "@amiceli/vitest-cucumber": "^6.5.0", + "vitest": "^4.0.18" + } +} diff --git a/bdd/steps/entity-predicate.steps.ts b/bdd/steps/entity-predicate.steps.ts new file mode 100644 index 000000000..d4d732960 --- /dev/null +++ b/bdd/steps/entity-predicate.steps.ts @@ -0,0 +1,50 @@ +/** + * Smoke spec — proves the Gherkin -> Vitest binding runs end to end against + * real product code, with no devnet and no network. + * + * Binds bdd/features/entity-predicate.feature to the pure helpers in + * packages/core/src/entity-predicate.ts (the OT-RFC-43/44 dual-read invariant). + */ +import { describeFeature, loadFeature } from '@amiceli/vitest-cucumber'; +import { expect } from 'vitest'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { isAssertionEntityPredicate, isEntityPredicate } from '@origintrail-official/dkg-core'; + +const here = dirname(fileURLToPath(import.meta.url)); +const feature = await loadFeature(resolve(here, '../features/entity-predicate.feature')); + +describeFeature(feature, ({ Background, ScenarioOutline }) => { + Background(({ Given }) => { + Given('the entity-predicate migration helpers', () => { + expect(typeof isEntityPredicate).toBe('function'); + expect(typeof isAssertionEntityPredicate).toBe('function'); + }); + }); + + ScenarioOutline( + 'A KA entity-member predicate is recognised across the rename', + ({ When, Then }, variables) => { + let result = false; + When('I check whether "" is a KA entity predicate', () => { + result = isEntityPredicate(String(variables.iri)); + }); + Then('the recognition result is ""', () => { + expect(String(result)).toBe(String(variables.expected)); + }); + }, + ); + + ScenarioOutline( + 'An assertion-seal entity predicate is recognised across the rename', + ({ When, Then }, variables) => { + let result = false; + When('I check whether "" is an assertion-seal entity predicate', () => { + result = isAssertionEntityPredicate(String(variables.iri)); + }); + Then('the recognition result is ""', () => { + expect(String(result)).toBe(String(variables.expected)); + }); + }, + ); +}); diff --git a/bdd/steps/ka-lifecycle.steps.ts b/bdd/steps/ka-lifecycle.steps.ts new file mode 100644 index 000000000..e1335a594 --- /dev/null +++ b/bdd/steps/ka-lifecycle.steps.ts @@ -0,0 +1,161 @@ +/** + * @devnet spec — the real KA publish lifecycle expressed as behaviour. + * + * Binds bdd/features/ka-lifecycle.feature to the same daemon routes that + * devnet/v10-core-flows' fullPublish() exercises, sharing the produced + * assertion + kaId through per-scenario closure state (the test "World"). + * + * Tag-gated: if no devnet is provisioned on disk, the @devnet scenarios are + * excluded (skipped cleanly) instead of failing — mirroring how the existing + * devnet suites guard, but as a skip rather than a hard error. + */ +import { describeFeature, loadFeature } from '@amiceli/vitest-cucumber'; +import { expect } from 'vitest'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { + CONTEXT_GRAPH, + type DevnetNode, + detectDevnet, + ensureIdentity, + postJson, + probeStatus, +} from '../support/world.js'; + +const here = dirname(fileURLToPath(import.meta.url)); +const feature = await loadFeature(resolve(here, '../features/ka-lifecycle.feature')); + +// Gate on a LIVE probe, not just on-disk provisioning: a stopped-but-provisioned +// cluster must skip cleanly (not fail in BeforeAllScenarios). detectDevnet() is +// the cheap disk check; probeStatus() confirms the node actually answers. +const disk = detectDevnet(); +const probe: typeof disk = + disk.available && disk.node && !(await probeStatus(disk.node)) + ? { + available: false, + reason: `node${disk.node.num} provisioned but not responding on ${disk.node.api}`, + } + : disk; +const excludeTags = probe.available ? [] : ['devnet']; +if (!probe.available) { + console.warn(`[bdd] @devnet scenarios skipped — ${probe.reason}`); +} + +describeFeature( + feature, + ({ Background, Scenario, BeforeAllScenarios }) => { + // Accessor instead of an unsafe cast: only ever invoked when the @devnet + // scenario actually runs (which only happens when a devnet is present). + const node = (): DevnetNode => { + if (!probe.node) { + throw new Error('devnet node unavailable — scenario should have been skipped'); + } + return probe.node; + }; + + // Register setup only when a devnet is present. When it is absent the + // scenario is excluded, and this hook must not run at all. + if (probe.available) { + BeforeAllScenarios(async () => { + if (!(await probeStatus(node()))) { + throw new Error( + `devnet node${node().num} (${node().api}) is provisioned but not answering /api/status`, + ); + } + // The chain-publish step needs an on-chain identity; bootstrap usually + // provides it, this is an idempotent safety net. + await ensureIdentity(node()); + }); + } + + Background(({ Given, And }) => { + Given('a reachable devnet node with an authenticated agent', () => { + expect(probe.node?.api, 'no devnet node resolved').toBeTruthy(); + }); + And('the "devnet-test" context graph', () => { + expect(CONTEXT_GRAPH).toBe('devnet-test'); + }); + }); + + Scenario( + 'A signed assertion becomes a confirmed on-chain Knowledge Asset', + ({ Given, When, Then, And }) => { + // Per-scenario World — declared here so each scenario gets fresh state + // (no bleed across scenarios as more are added). + const contextGraphId = CONTEXT_GRAPH; + let assertionName = ''; + let finalizeRes: { merkleRoot?: string; eip712Digest?: string } = {}; + let promoteRes: { promotedCount?: number } = {}; + let publishRes: { kaId?: string; status?: string } = {}; + + Given('a fresh draft assertion in the context graph', async () => { + assertionName = `bdd-ka-${Date.now()}`; + const r = await postJson(node(), '/api/assertion/create', { + contextGraphId, + name: assertionName, + }); + expect(r.status, JSON.stringify(r.body)).toBe(200); + }); + + When('I write 2 entity quads to the assertion', async () => { + const subject = `urn:test:bdd:${assertionName}`; + const quads = [ + { subject, predicate: 'http://schema.org/name', object: `"${assertionName}"`, graph: '' }, + { subject, predicate: 'http://schema.org/value', object: '"bdd pilot"', graph: '' }, + ]; + const r = await postJson(node(), `/api/assertion/${assertionName}/write`, { + contextGraphId, + quads, + }); + expect(r.status, JSON.stringify(r.body)).toBe(200); + expect(Number(r.body?.written ?? 0)).toBeGreaterThanOrEqual(2); + }); + + And('I finalize the assertion', async () => { + const r = await postJson(node(), `/api/assertion/${assertionName}/finalize`, { + contextGraphId, + }); + expect(r.status, JSON.stringify(r.body)).toBe(200); + finalizeRes = r.body; + }); + + Then('the assertion has a merkle root and an EIP-712 digest', () => { + expect(finalizeRes.merkleRoot, JSON.stringify(finalizeRes)).toMatch(/^0x[0-9a-fA-F]+$/); + expect(finalizeRes.eip712Digest, JSON.stringify(finalizeRes)).toMatch(/^0x[0-9a-fA-F]+$/); + }); + + When('I promote the assertion to shared working memory', async () => { + const r = await postJson(node(), `/api/assertion/${assertionName}/promote`, { + contextGraphId, + }); + expect(r.status, JSON.stringify(r.body)).toBe(200); + promoteRes = r.body; + }); + + Then('at least 1 share is created', () => { + expect(Number(promoteRes.promotedCount ?? 0)).toBeGreaterThanOrEqual(1); + }); + + When('I publish the assertion to the chain', async () => { + const r = await postJson(node(), '/api/shared-memory/publish', { + contextGraphId, + assertionName, + }); + expect(r.status, JSON.stringify(r.body)).toBe(200); + publishRes = r.body; + }); + + Then('a knowledge asset id is returned', () => { + expect(BigInt(publishRes.kaId ?? '0'), JSON.stringify(publishRes)).toBeGreaterThan(0n); + }); + + And('the knowledge asset status is one of "confirmed,tentative,finalized"', () => { + expect(['confirmed', 'tentative', 'finalized']).toContain( + String(publishRes.status).toLowerCase(), + ); + }); + }, + ); + }, + { excludeTags }, +); diff --git a/bdd/support/world.ts b/bdd/support/world.ts new file mode 100644 index 000000000..870c30725 --- /dev/null +++ b/bdd/support/world.ts @@ -0,0 +1,157 @@ +/** + * Shared "World" for the BDD pilot's @devnet steps. + * + * Pure Node (global fetch + node:fs) — no test framework imports, so it can be + * reused by any step file. Mirrors the connection + gating logic that + * devnet/v10-*-flows do inline (readNodeConfig / readDevnetToken / ensureIdentity + * / postJson), but exported so the Gherkin step layer can call it directly. + * + * Kept deliberately faithful to devnet/v10-end-to-end/automated.test.ts: + * - apiPort comes from `config.apiPort` + * - auth.token may contain `#` comment lines; take the first non-empty, + * non-comment line (matches readNodeConfig at lines 122-129) + */ +import { existsSync, readFileSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = resolve(HERE, '../..'); +const DEVNET_DIR = resolve(REPO_ROOT, '.devnet'); + +export const CONTEXT_GRAPH = 'devnet-test'; + +export interface DevnetNode { + num: number; + apiPort: number; + authToken: string; + api: string; +} + +export interface DevnetProbe { + available: boolean; + reason?: string; + node?: DevnetNode; +} + +/** First non-empty, non-comment line — matches the harness's auth.token parsing. */ +function readAuthToken(home: string): string { + const path = join(home, 'auth.token'); + if (!existsSync(path)) return ''; + return ( + readFileSync(path, 'utf8') + .split('\n') + .map((l) => l.trim()) + .find((l) => l.length > 0 && !l.startsWith('#')) ?? '' + ); +} + +/** + * Cheap, synchronous gate — does a devnet appear to be provisioned on disk? + * Used to decide whether to include or skip the @devnet scenarios. The async + * health check (probeStatus) confirms the node is actually answering. + */ +export function detectDevnet(num = 1): DevnetProbe { + if (!existsSync(DEVNET_DIR)) return { available: false, reason: `no ${DEVNET_DIR}` }; + const home = join(DEVNET_DIR, `node${num}`); + const cfgPath = join(home, 'config.json'); + if (!existsSync(cfgPath)) return { available: false, reason: `no ${cfgPath}` }; + + let apiPort: number | undefined; + try { + const cfg = JSON.parse(readFileSync(cfgPath, 'utf8')) as { apiPort?: number }; + apiPort = cfg.apiPort; + } catch (e) { + return { available: false, reason: `unreadable config: ${(e as Error).message}` }; + } + if (!apiPort) return { available: false, reason: 'no apiPort in node config' }; + + return { + available: true, + node: { num, apiPort, authToken: readAuthToken(home), api: `http://127.0.0.1:${apiPort}` }, + }; +} + +function headers(node: DevnetNode): Record { + const h: Record = { 'content-type': 'application/json' }; + if (node.authToken) h.authorization = `Bearer ${node.authToken}`; + return h; +} + +export interface HttpResult { + status: number; + body: any; +} + +export async function postJson(node: DevnetNode, path: string, body: unknown): Promise { + const res = await fetch(`${node.api}${path}`, { + method: 'POST', + headers: headers(node), + body: JSON.stringify(body), + }); + return { status: res.status, body: await parse(res) }; +} + +export async function getJson(node: DevnetNode, path: string): Promise { + const res = await fetch(`${node.api}${path}`, { headers: headers(node) }); + return { status: res.status, body: await parse(res) }; +} + +async function parse(res: Response): Promise { + const text = await res.text(); + if (!text) return undefined; + try { + return JSON.parse(text); + } catch { + return text; + } +} + +/** + * Async health check — true only if /api/status answers 2xx within `timeoutMs`. + * A provisioned-but-stopped node fails fast (connection refused) and returns + * false, so callers can downgrade it to a skip rather than a hard error. + */ +export async function probeStatus(node: DevnetNode, timeoutMs = 3000): Promise { + const ctrl = new AbortController(); + const timer = setTimeout(() => ctrl.abort(), timeoutMs); + try { + const res = await fetch(`${node.api}/api/status`, { + headers: headers(node), + signal: ctrl.signal, + }); + return res.status >= 200 && res.status < 300; + } catch { + return false; + } finally { + clearTimeout(timer); + } +} + +/** + * Idempotent identity registration — mirrors ensureIdentity() in the devnet + * harness. The chain-publish step needs the node to have an on-chain identity; + * the bootstrap script normally provides it, this is a safety net. Returns the + * identityId (0n if the node never registers in time). + */ +export async function ensureIdentity(node: DevnetNode, timeoutMs = 30_000): Promise { + const current = await readIdentity(node); + if (current > 0n) return current; + await postJson(node, '/api/identity/ensure', {}); + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + const id = await readIdentity(node); + if (id > 0n) return id; + await new Promise((r) => setTimeout(r, 1000)); + } + return 0n; +} + +async function readIdentity(node: DevnetNode): Promise { + try { + const { body } = await getJson(node, '/api/status'); + return BigInt(body?.identityId ?? 0); + } catch { + return 0n; + } +} diff --git a/bdd/turbo.json b/bdd/turbo.json new file mode 100644 index 000000000..8370b0c2c --- /dev/null +++ b/bdd/turbo.json @@ -0,0 +1,8 @@ +{ + "extends": ["//"], + "tasks": { + "test": { + "dependsOn": ["^build"] + } + } +} diff --git a/bdd/vitest.config.ts b/bdd/vitest.config.ts new file mode 100644 index 000000000..0487ac72d --- /dev/null +++ b/bdd/vitest.config.ts @@ -0,0 +1,30 @@ +import { defineConfig } from 'vitest/config'; +import { resolve } from 'node:path'; + +/** + * BDD pilot — runs Gherkin .feature specs on the project's existing Vitest + * runner via @amiceli/vitest-cucumber (no second runner, no extra CI lane). + * + * - steps/entity-predicate.steps.ts -> pure smoke spec, always runnable + * - steps/ka-lifecycle.steps.ts -> @devnet, skipped unless a devnet is up + * + * Smoke only: pnpm --filter @origintrail-official/dkg-bdd test (CI default) + * All specs: pnpm --filter @origintrail-official/dkg-bdd test:all + * Devnet spec: ./scripts/devnet.sh start 6 (then) ... test:devnet + * + * Timeouts/pool mirror the devnet scenario configs so the @devnet spec has + * room to boot + round-trip; the smoke spec finishes in milliseconds. + */ +export default defineConfig({ + test: { + include: [resolve(import.meta.dirname, 'steps/**/*.steps.ts')], + testTimeout: 900_000, + hookTimeout: 300_000, + pool: 'forks', + sequence: { concurrent: false }, + globals: false, + }, + resolve: { + modules: [resolve(import.meta.dirname, '../node_modules'), 'node_modules'], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e10b10d5..e8bfb41ca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,6 +68,19 @@ importers: specifier: ^4.0.18 version: 4.0.18(@types/node@22.19.11)(happy-dom@20.8.9(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0) + bdd: + dependencies: + '@origintrail-official/dkg-core': + specifier: workspace:* + version: link:../packages/core + devDependencies: + '@amiceli/vitest-cucumber': + specifier: ^6.5.0 + version: 6.5.0(vitest@4.0.18(@types/node@22.19.11)(happy-dom@20.8.9(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)) + vitest: + specifier: ^4.0.18 + version: 4.0.18(@types/node@22.19.11)(happy-dom@20.8.9(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0) + demo: dependencies: '@multiformats/multiaddr': @@ -872,6 +885,12 @@ packages: '@adraffy/ens-normalize@1.11.1': resolution: {integrity: sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==} + '@amiceli/vitest-cucumber@6.5.0': + resolution: {integrity: sha512-1883MTz+PsfTcR5C2wYlHh8rQzOEYOogdDhAikGWsx5UfTzIusP4HLH32kpcAPOBvHUpguYcA8Zmw5JxEydbzg==} + hasBin: true + peerDependencies: + vitest: ^4.0.4 + '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -2510,6 +2529,9 @@ packages: resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} engines: {node: '>=14.16'} + '@ts-morph/common@0.29.0': + resolution: {integrity: sha512-35oUmphHbJvQ/+UTwFNme/t2p3FoKiGJ5auTjjpNTop2dyREspirjMy82PLSC1pnDJ8ah1GU98hwpVt64YXQsg==} + '@tsconfig/node10@1.0.12': resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} @@ -3111,6 +3133,10 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + callsites@4.2.0: + resolution: {integrity: sha512-kfzR4zzQtAE9PC7CzZsjl3aBNbXWuXiSeOCdLcPpBfGW8YuCqQHcRPFDbr/BPVmd3EEPVpuFzLyuT/cUhPr4OQ==} + engines: {node: '>=12.20'} + camelcase@6.3.0: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} @@ -3220,6 +3246,9 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + code-block-writer@13.0.3: + resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} + color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -5142,10 +5171,16 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parsecurrency@1.1.1: + resolution: {integrity: sha512-IAw/8PSFgiko70KfZGv63rbEXhmVu+zpb42PvEtgHAm83Mze3eQJHWV1ZoOhPnrYeOyufvv0GS6hZDuQOdBH4Q==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -6036,6 +6071,9 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-morph@28.0.0: + resolution: {integrity: sha512-Wp3tnZ2bzwxyTZMtgWVzXDfm7lB1Drz+y9DmmYH/L702PQhPyVrp3pkou3yIz4qjS14GY9kcpmLiOOMvl8oG1g==} + ts-node@10.9.2: resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true @@ -6635,6 +6673,14 @@ snapshots: '@adraffy/ens-normalize@1.11.1': {} + '@amiceli/vitest-cucumber@6.5.0(vitest@4.0.18(@types/node@22.19.11)(happy-dom@20.8.9(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0))': + dependencies: + callsites: 4.2.0 + minimist: 1.2.8 + parsecurrency: 1.1.1 + ts-morph: 28.0.0 + vitest: 4.0.18(@types/node@22.19.11)(happy-dom@20.8.9(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0) + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -8357,6 +8403,12 @@ snapshots: dependencies: defer-to-connect: 2.0.1 + '@ts-morph/common@0.29.0': + dependencies: + minimatch: 10.2.3 + path-browserify: 1.0.1 + tinyglobby: 0.2.17 + '@tsconfig/node10@1.0.12': {} '@tsconfig/node12@1.0.11': {} @@ -8984,6 +9036,8 @@ snapshots: callsites@3.1.0: {} + callsites@4.2.0: {} + camelcase@6.3.0: {} caniuse-lite@1.0.30001774: {} @@ -9095,6 +9149,8 @@ snapshots: clsx@2.1.1: {} + code-block-writer@13.0.3: {} + color-convert@1.9.3: dependencies: color-name: 1.1.3 @@ -11572,8 +11628,12 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parsecurrency@1.1.1: {} + parseurl@1.3.3: {} + path-browserify@1.0.1: {} + path-exists@4.0.0: {} path-is-absolute@1.0.1: {} @@ -12615,6 +12675,11 @@ snapshots: ts-interface-checker@0.1.13: {} + ts-morph@28.0.0: + dependencies: + '@ts-morph/common': 0.29.0 + code-block-writer: 13.0.3 + ts-node@10.9.2(@types/node@22.19.11)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a8fe2719b..5bf25cb46 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,6 +3,7 @@ packages: - "packages/cli/test-fixtures/sample-kafka-plugin" - "packages/cli/test-fixtures/sample-kafka-extension" - "demo" + - "bdd" - "devnet/agent-provenance" - "devnet/core-peers-features" - "devnet/v10-core-flows"