@hardkas/testing 0.2.2-alpha.1 → 0.4.0-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Generates artifacts with semantic or structural flaws for adversarial testing.
3
+ */
4
+ declare const AdversarialFixtures: {
5
+ /**
6
+ * Circular lineage: A -> B -> A
7
+ */
8
+ circularLineage(): {
9
+ artifactA: any;
10
+ artifactB: any;
11
+ };
12
+ /**
13
+ * Artifact where the contentHash does not match the actual content calculation.
14
+ */
15
+ hashMismatch(): any;
16
+ /**
17
+ * Artifact with a parent from a different network (Security Violation).
18
+ */
19
+ crossNetworkLineage(): {
20
+ parent: any;
21
+ child: any;
22
+ };
23
+ /**
24
+ * Trace with duplicate event IDs (Corruption).
25
+ */
26
+ duplicateEventTrace(): {
27
+ schema: string;
28
+ workflowId: string;
29
+ events: {
30
+ eventId: string;
31
+ kind: string;
32
+ }[];
33
+ };
34
+ /**
35
+ * Malformed JSONL snippet (truncated).
36
+ */
37
+ malformedJsonl(): string;
38
+ /**
39
+ * Lineage with sequence rollback (corrupted history).
40
+ */
41
+ sequenceRollback(): ({
42
+ artifactId: string;
43
+ lineage: {
44
+ sequenceId: number;
45
+ contentHash: string;
46
+ parentArtifactId?: never;
47
+ };
48
+ schema: string;
49
+ version: string;
50
+ networkId: string;
51
+ mode: string;
52
+ } | {
53
+ artifactId: string;
54
+ lineage: {
55
+ sequenceId: number;
56
+ contentHash: string;
57
+ parentArtifactId: string;
58
+ };
59
+ schema: string;
60
+ version: string;
61
+ networkId: string;
62
+ mode: string;
63
+ })[];
64
+ /**
65
+ * Artifact with a future timestamp (anomaly).
66
+ * Intentionally uses Date.now() to generate a timestamp far in the future.
67
+ */
68
+ futureTimestamp(): {
69
+ schema: string;
70
+ version: string;
71
+ artifactId: string;
72
+ contentHash: string;
73
+ networkId: string;
74
+ mode: string;
75
+ createdAt: string;
76
+ };
77
+ /**
78
+ * Artifact with an unsupported version.
79
+ */
80
+ unsupportedVersion(): {
81
+ schema: string;
82
+ version: string;
83
+ artifactId: string;
84
+ contentHash: string;
85
+ networkId: string;
86
+ mode: string;
87
+ };
88
+ };
89
+
90
+ export { AdversarialFixtures };
@@ -0,0 +1,6 @@
1
+ import {
2
+ AdversarialFixtures
3
+ } from "./chunk-3EK7ATR5.js";
4
+ export {
5
+ AdversarialFixtures
6
+ };
@@ -0,0 +1,108 @@
1
+ import {
2
+ createTestHarness
3
+ } from "./chunk-CUJL53GG.js";
4
+
5
+ // src/reproducibility.ts
6
+ import { calculateContentHash } from "@hardkas/artifacts";
7
+ import { runLinearChain, runWideDag, profileMass } from "@hardkas/simulator";
8
+ function generateReproducibilityReport() {
9
+ const l1Plan = {
10
+ schema: "hardkas.txPlan",
11
+ networkId: "simnet",
12
+ mode: "simulated",
13
+ from: { address: "kaspatest:qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
14
+ to: { address: "kaspatest:qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
15
+ amountSompi: "100000000",
16
+ inputs: [
17
+ { outpoint: { transactionId: "a".repeat(64), index: 0 }, amountSompi: "200000000" }
18
+ ],
19
+ outputs: [
20
+ { address: "kaspatest:qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg", amountSompi: "100000000" },
21
+ { address: "kaspatest:qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv", amountSompi: "99998000" }
22
+ ],
23
+ estimatedMass: "2000",
24
+ estimatedFeeSompi: "2000"
25
+ };
26
+ const l1Signed = {
27
+ schema: "hardkas.signedTx",
28
+ networkId: "simnet",
29
+ mode: "simulated",
30
+ rawTransaction: "0".repeat(128),
31
+ inputs: l1Plan.inputs,
32
+ outputs: l1Plan.outputs,
33
+ mass: "2000",
34
+ feeSompi: "2000"
35
+ };
36
+ const igraPlan = {
37
+ schema: "hardkas.igraTxPlan.v1",
38
+ networkId: "simnet",
39
+ mode: "l2-rpc",
40
+ l2Network: "igra-devnet",
41
+ chainId: 42069,
42
+ request: {
43
+ from: "0x1234567890123456789012345678901234567890",
44
+ to: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
45
+ data: "0x",
46
+ valueWei: "1000000000000000000"
47
+ },
48
+ status: "built"
49
+ };
50
+ const linearResult = runLinearChain({ name: "repro-linear", blockCount: 10 });
51
+ const wideResult = runWideDag({ name: "repro-wide", blockCount: 10, k: 18 });
52
+ const linearTotal = linearResult.metrics.blueBlocks + linearResult.metrics.redBlocks;
53
+ const linearRedPpm = linearTotal > 0 ? Math.trunc(linearResult.metrics.redBlocks * 1e6 / linearTotal) : 0;
54
+ const wideTotal = wideResult.metrics.blueBlocks + wideResult.metrics.redBlocks;
55
+ const wideRedPpm = wideTotal > 0 ? Math.trunc(wideResult.metrics.redBlocks * 1e6 / wideTotal) : 0;
56
+ const massResult = profileMass({ inputCount: 3, outputCount: 2, payloadBytes: 0, feeRate: 1n });
57
+ const nestedObj = {
58
+ z: { b: 2, a: 1 },
59
+ y: [3, 1, 2],
60
+ x: null,
61
+ w: "test",
62
+ v: 123456789012345678901234567890n
63
+ };
64
+ const harness = createTestHarness({ accounts: 3, initialBalance: 100000000000n });
65
+ const names = harness.accountNames();
66
+ const txResult = harness.send({
67
+ from: names[0],
68
+ to: names[1],
69
+ amountSompi: 10000000000n
70
+ });
71
+ return {
72
+ proofVersion: "repro-v0",
73
+ hardkasVersion: "0.4.0-alpha",
74
+ artifacts: {
75
+ l1Plan: calculateContentHash(l1Plan),
76
+ l1Signed: calculateContentHash(l1Signed),
77
+ igraPlan: calculateContentHash(igraPlan),
78
+ dagLinearScenario: calculateContentHash({
79
+ totalBlocks: linearResult.metrics.totalBlocks,
80
+ blueBlocks: linearResult.metrics.blueBlocks,
81
+ redBlocks: linearResult.metrics.redBlocks,
82
+ redRatioPpm: linearRedPpm,
83
+ selectedChainLength: linearResult.metrics.selectedChainLength
84
+ }),
85
+ dagWideScenario: calculateContentHash({
86
+ totalBlocks: wideResult.metrics.totalBlocks,
87
+ blueBlocks: wideResult.metrics.blueBlocks,
88
+ redBlocks: wideResult.metrics.redBlocks,
89
+ redRatioPpm: wideRedPpm
90
+ }),
91
+ massProfile: calculateContentHash({
92
+ totalMass: massResult.totalMass.toString(),
93
+ inputMass: massResult.inputMass.toString(),
94
+ outputMass: massResult.outputMass.toString(),
95
+ estimatedFeeSompi: massResult.estimatedFeeSompi.toString()
96
+ }),
97
+ canonicalNested: calculateContentHash(nestedObj),
98
+ simulatedTxReceipt: txResult.ok ? calculateContentHash({
99
+ status: txResult.receipt.status,
100
+ txId: txResult.receipt.txId
101
+ }) : "TX_FAILED"
102
+ }
103
+ };
104
+ }
105
+
106
+ export {
107
+ generateReproducibilityReport
108
+ };
@@ -0,0 +1,152 @@
1
+ // src/adversarial-fixtures.ts
2
+ import { calculateContentHash, CURRENT_HASH_VERSION, ARTIFACT_VERSION } from "@hardkas/artifacts";
3
+ var AdversarialFixtures = {
4
+ /**
5
+ * Circular lineage: A -> B -> A
6
+ */
7
+ circularLineage() {
8
+ const artifactA = {
9
+ schema: "hardkas.txPlan",
10
+ version: ARTIFACT_VERSION,
11
+ artifactId: "art-a",
12
+ contentHash: "hash-a",
13
+ networkId: "simnet",
14
+ mode: "simulated",
15
+ lineage: { parentArtifactId: "art-b" }
16
+ };
17
+ const artifactB = {
18
+ schema: "hardkas.txPlan",
19
+ version: ARTIFACT_VERSION,
20
+ artifactId: "art-b",
21
+ contentHash: "hash-b",
22
+ networkId: "simnet",
23
+ mode: "simulated",
24
+ lineage: { parentArtifactId: "art-a" }
25
+ };
26
+ return { artifactA, artifactB };
27
+ },
28
+ /**
29
+ * Artifact where the contentHash does not match the actual content calculation.
30
+ */
31
+ hashMismatch() {
32
+ const artifact = {
33
+ schema: "hardkas.txPlan",
34
+ version: ARTIFACT_VERSION,
35
+ networkId: "simnet",
36
+ mode: "simulated",
37
+ amountSompi: "1000",
38
+ estimatedFeeSompi: "1",
39
+ estimatedMass: "1",
40
+ from: { address: "kaspasim:qz0s9xrz5y5e8dq5azmpg756aeepm6fesq82ye7wv" },
41
+ to: { address: "kaspasim:qq0d6h0prjm5mpdld5pncst3adu0yam6xch9fkr6eg" },
42
+ inputs: [],
43
+ outputs: [],
44
+ hashVersion: CURRENT_HASH_VERSION
45
+ };
46
+ const realHash = calculateContentHash(artifact, CURRENT_HASH_VERSION);
47
+ artifact.contentHash = "f" + realHash.slice(1);
48
+ artifact.artifactId = `plan-${artifact.contentHash.slice(0, 16)}`;
49
+ artifact.planId = artifact.artifactId;
50
+ return artifact;
51
+ },
52
+ /**
53
+ * Artifact with a parent from a different network (Security Violation).
54
+ */
55
+ crossNetworkLineage() {
56
+ const parent = {
57
+ schema: "hardkas.txPlan",
58
+ version: ARTIFACT_VERSION,
59
+ artifactId: "parent-mainnet",
60
+ contentHash: "hash-mainnet",
61
+ networkId: "mainnet",
62
+ mode: "l1-rpc"
63
+ };
64
+ const child = {
65
+ schema: "hardkas.signedTx",
66
+ version: ARTIFACT_VERSION,
67
+ artifactId: "child-simnet",
68
+ contentHash: "hash-simnet",
69
+ networkId: "simnet",
70
+ mode: "simulated",
71
+ lineage: {
72
+ artifactId: "hash-simnet",
73
+ parentArtifactId: "parent-mainnet",
74
+ lineageId: "b".repeat(64),
75
+ rootArtifactId: "c".repeat(64)
76
+ }
77
+ };
78
+ return { parent, child };
79
+ },
80
+ /**
81
+ * Trace with duplicate event IDs (Corruption).
82
+ */
83
+ duplicateEventTrace() {
84
+ return {
85
+ schema: "hardkas.trace",
86
+ workflowId: "wf-1",
87
+ events: [
88
+ { eventId: "ev-1", kind: "start" },
89
+ { eventId: "ev-1", kind: "step" }
90
+ // Duplicate ID
91
+ ]
92
+ };
93
+ },
94
+ /**
95
+ * Malformed JSONL snippet (truncated).
96
+ */
97
+ malformedJsonl() {
98
+ return `{"eventId":"ev-1","kind":"start"}
99
+ {"eventId":"ev-2","kind":"step",`;
100
+ },
101
+ /**
102
+ * Lineage with sequence rollback (corrupted history).
103
+ */
104
+ sequenceRollback() {
105
+ const common = {
106
+ schema: "hardkas.txPlan",
107
+ version: ARTIFACT_VERSION,
108
+ networkId: "simnet",
109
+ mode: "simulated"
110
+ };
111
+ return [
112
+ { ...common, artifactId: "art-1", lineage: { sequenceId: 1, contentHash: "hash-1" } },
113
+ { ...common, artifactId: "art-2", lineage: { sequenceId: 2, contentHash: "hash-2", parentArtifactId: "art-1" } },
114
+ { ...common, artifactId: "art-3", lineage: { sequenceId: 2, contentHash: "hash-3-BAD", parentArtifactId: "art-1" } }
115
+ // Duplicate sequenceId 2
116
+ ];
117
+ },
118
+ /**
119
+ * Artifact with a future timestamp (anomaly).
120
+ * Intentionally uses Date.now() to generate a timestamp far in the future.
121
+ */
122
+ futureTimestamp() {
123
+ const farFuture = new Date(Date.now() + 1e3 * 60 * 60 * 24 * 365 * 10).toISOString();
124
+ return {
125
+ schema: "hardkas.txPlan",
126
+ version: ARTIFACT_VERSION,
127
+ artifactId: "art-future",
128
+ contentHash: "hash-future",
129
+ networkId: "simnet",
130
+ mode: "simulated",
131
+ createdAt: farFuture
132
+ };
133
+ },
134
+ /**
135
+ * Artifact with an unsupported version.
136
+ */
137
+ unsupportedVersion() {
138
+ return {
139
+ schema: "hardkas.txPlan",
140
+ version: "99.9.9",
141
+ // Future version
142
+ artifactId: "art-vnext",
143
+ contentHash: "hash-vnext",
144
+ networkId: "simnet",
145
+ mode: "simulated"
146
+ };
147
+ }
148
+ };
149
+
150
+ export {
151
+ AdversarialFixtures
152
+ };
@@ -0,0 +1,90 @@
1
+ // src/harness.ts
2
+ import {
3
+ createInitialLocalnetState,
4
+ applySimulatedPayment,
5
+ getAccountBalanceSompi,
6
+ createLocalnetSnapshot
7
+ } from "@hardkas/localnet";
8
+ var massRecords = [];
9
+ var massTrackingEnabled = false;
10
+ function enableMassTracking() {
11
+ massTrackingEnabled = true;
12
+ }
13
+ function disableMassTracking() {
14
+ massTrackingEnabled = false;
15
+ }
16
+ function getMassRecords() {
17
+ return [...massRecords];
18
+ }
19
+ function clearMassRecords() {
20
+ massRecords = [];
21
+ }
22
+ function createTestHarness(config) {
23
+ const accountCount = config?.accounts ?? 3;
24
+ const initialBalanceSompi = config?.initialBalance ?? 100000000000n;
25
+ let currentState = createInitialLocalnetState({
26
+ accounts: accountCount,
27
+ initialBalanceSompi
28
+ });
29
+ if (config?.networkId) {
30
+ currentState.networkId = config.networkId;
31
+ }
32
+ const initialState = structuredClone(currentState);
33
+ const harness = {
34
+ get state() {
35
+ return currentState;
36
+ },
37
+ send(opts) {
38
+ const preBalance = {
39
+ from: getAccountBalanceSompi(currentState, opts.from),
40
+ to: getAccountBalanceSompi(currentState, opts.to)
41
+ };
42
+ const result = applySimulatedPayment(currentState, opts);
43
+ if (result.ok) {
44
+ currentState = result.state;
45
+ }
46
+ const postBalance = {
47
+ from: getAccountBalanceSompi(currentState, opts.from),
48
+ to: getAccountBalanceSompi(currentState, opts.to)
49
+ };
50
+ if (result.ok && massTrackingEnabled && result.planArtifact) {
51
+ massRecords.push({
52
+ txId: result.receipt.txId,
53
+ inputCount: result.planArtifact.inputs.length,
54
+ outputCount: result.planArtifact.outputs.length,
55
+ estimatedMass: BigInt(result.planArtifact.estimatedMass),
56
+ estimatedFeeSompi: BigInt(result.planArtifact.estimatedFeeSompi),
57
+ timestamp: Date.now()
58
+ });
59
+ }
60
+ return {
61
+ ok: result.ok,
62
+ receipt: result.receipt,
63
+ plan: result.planArtifact,
64
+ preBalance,
65
+ postBalance
66
+ };
67
+ },
68
+ balanceOf(name) {
69
+ return getAccountBalanceSompi(currentState, name);
70
+ },
71
+ accountNames() {
72
+ return currentState.accounts.map((a) => a.name);
73
+ },
74
+ snapshot() {
75
+ return createLocalnetSnapshot(currentState);
76
+ },
77
+ reset() {
78
+ currentState = structuredClone(initialState);
79
+ }
80
+ };
81
+ return harness;
82
+ }
83
+
84
+ export {
85
+ enableMassTracking,
86
+ disableMassTracking,
87
+ getMassRecords,
88
+ clearMassRecords,
89
+ createTestHarness
90
+ };
@@ -0,0 +1,103 @@
1
+ // src/setup.ts
2
+ import { expect } from "vitest";
3
+
4
+ // src/matchers.ts
5
+ var hardKasMatchers = {
6
+ toBeAccepted(received) {
7
+ const pass = received?.status === "accepted";
8
+ return {
9
+ pass,
10
+ message: () => pass ? `Expected receipt NOT to be accepted, but status is "${received.status}"` : `Expected receipt to be accepted, but status is "${received?.status ?? "undefined"}"`
11
+ };
12
+ },
13
+ toBeFailed(received) {
14
+ const pass = received?.status === "failed";
15
+ return {
16
+ pass,
17
+ message: () => pass ? `Expected receipt NOT to be failed, but status is "failed"` : `Expected receipt to be failed, but status is "${received?.status ?? "undefined"}"`
18
+ };
19
+ },
20
+ toHaveValidTxId(received) {
21
+ const txId = received?.txId || (typeof received === "string" ? received : "");
22
+ const pass = typeof txId === "string" && (txId.startsWith("simtx_") || /^[0-9a-fA-F]{64}$/.test(txId));
23
+ return {
24
+ pass,
25
+ message: () => pass ? `Expected "${txId}" NOT to be a valid txId` : `Expected "${txId}" to be a valid txId (starts with "simtx_" or is 64-char hex)`
26
+ };
27
+ },
28
+ toHaveValidContentHash(received) {
29
+ const hash = received?.contentHash || (typeof received === "string" ? received : "");
30
+ const pass = typeof hash === "string" && /^[0-9a-fA-F]{64}$/.test(hash);
31
+ return {
32
+ pass,
33
+ message: () => pass ? `Expected "${hash}" NOT to be a valid content hash` : `Expected "${hash}" to be a valid content hash (64-char hex)`
34
+ };
35
+ },
36
+ toPassLineageCheck(received) {
37
+ const pass = Array.isArray(received?.lineage) && received.lineage.length > 0;
38
+ return {
39
+ pass,
40
+ message: () => pass ? `Expected artifact NOT to pass lineage check` : `Expected artifact to pass lineage check (missing or empty lineage)`
41
+ };
42
+ },
43
+ toBeBlueBlock(received) {
44
+ const pass = received?.isBlue === true;
45
+ return {
46
+ pass,
47
+ message: () => pass ? `Expected block NOT to be blue, but it is` : `Expected block to be blue, but it is red or unknown`
48
+ };
49
+ },
50
+ toBeRedBlock(received) {
51
+ const pass = received?.isBlue === false;
52
+ return {
53
+ pass,
54
+ message: () => pass ? `Expected block NOT to be red, but it is` : `Expected block to be red, but it is blue or unknown`
55
+ };
56
+ },
57
+ toHaveIncreasedBy(received, amount) {
58
+ const actual = BigInt(received);
59
+ const pass = actual >= amount;
60
+ return {
61
+ pass,
62
+ message: () => pass ? `Expected increase NOT to be at least ${amount}, but got ${actual}` : `Expected increase to be at least ${amount}, but got ${actual}`
63
+ };
64
+ },
65
+ toHaveDecreasedBy(received, amount) {
66
+ const actual = BigInt(received);
67
+ const pass = actual >= amount;
68
+ return {
69
+ pass,
70
+ message: () => pass ? `Expected decrease NOT to be at least ${amount}, but got ${actual}` : `Expected decrease to be at least ${amount}, but got ${actual}`
71
+ };
72
+ },
73
+ toHaveNoRedBlocks(received) {
74
+ const redBlocks = Object.values(received?.blocks || {}).filter((b) => b.isBlue === false);
75
+ const pass = redBlocks.length === 0;
76
+ return {
77
+ pass,
78
+ message: () => pass ? `Expected DAG to have red blocks, but it has none` : `Expected DAG to have no red blocks, but found ${redBlocks.length}`
79
+ };
80
+ },
81
+ toHaveDagWidth(received, width) {
82
+ const allBlockIds = Object.keys(received?.blocks || {});
83
+ const parentIds = /* @__PURE__ */ new Set();
84
+ for (const block of Object.values(received?.blocks || {})) {
85
+ for (const p of block.parents) {
86
+ parentIds.add(p);
87
+ }
88
+ }
89
+ const tips = allBlockIds.filter((id) => !parentIds.has(id));
90
+ const pass = tips.length === width;
91
+ return {
92
+ pass,
93
+ message: () => pass ? `Expected DAG NOT to have width ${width}, but it does` : `Expected DAG to have width ${width}, but got ${tips.length}`
94
+ };
95
+ }
96
+ };
97
+
98
+ // src/setup.ts
99
+ expect.extend(hardKasMatchers);
100
+
101
+ export {
102
+ hardKasMatchers
103
+ };
@@ -0,0 +1,55 @@
1
+ import { LocalnetState } from '@hardkas/localnet';
2
+
3
+ interface HarnessConfig {
4
+ accounts?: number | undefined;
5
+ initialBalance?: bigint | undefined;
6
+ networkId?: string | undefined;
7
+ ghostdagK?: number | undefined;
8
+ }
9
+ interface TestHarness {
10
+ /** The current localnet state. */
11
+ state: LocalnetState;
12
+ /** Send KAS from one account to another. Returns the receipt. */
13
+ send(opts: {
14
+ from: string;
15
+ to: string;
16
+ amountSompi: bigint;
17
+ }): SendResult;
18
+ /** Get balance of an account by name (e.g., "alice"). */
19
+ balanceOf(name: string): bigint;
20
+ /** Get all account names. */
21
+ accountNames(): string[];
22
+ /** Take a snapshot of current state. */
23
+ snapshot(): any;
24
+ /** Reset to initial state. */
25
+ reset(): void;
26
+ }
27
+ interface SendResult {
28
+ ok: boolean;
29
+ receipt: any;
30
+ plan: any;
31
+ preBalance: {
32
+ from: bigint;
33
+ to: bigint;
34
+ };
35
+ postBalance: {
36
+ from: bigint;
37
+ to: bigint;
38
+ };
39
+ }
40
+ interface MassRecord {
41
+ testName?: string;
42
+ txId: string;
43
+ inputCount: number;
44
+ outputCount: number;
45
+ estimatedMass: bigint;
46
+ estimatedFeeSompi: bigint;
47
+ timestamp: number;
48
+ }
49
+ declare function enableMassTracking(): void;
50
+ declare function disableMassTracking(): void;
51
+ declare function getMassRecords(): MassRecord[];
52
+ declare function clearMassRecords(): void;
53
+ declare function createTestHarness(config?: HarnessConfig): TestHarness;
54
+
55
+ export { type HarnessConfig, type MassRecord, type SendResult, type TestHarness, clearMassRecords, createTestHarness, disableMassTracking, enableMassTracking, getMassRecords };
@@ -0,0 +1,14 @@
1
+ import {
2
+ clearMassRecords,
3
+ createTestHarness,
4
+ disableMassTracking,
5
+ enableMassTracking,
6
+ getMassRecords
7
+ } from "./chunk-CUJL53GG.js";
8
+ export {
9
+ clearMassRecords,
10
+ createTestHarness,
11
+ disableMassTracking,
12
+ enableMassTracking,
13
+ getMassRecords
14
+ };
package/dist/index.d.ts CHANGED
@@ -1,69 +1,100 @@
1
- import { Hardkas } from '@hardkas/sdk';
2
- import { CoreEvent, NetworkId } from '@hardkas/core';
3
- import { BaseArtifact } from '@hardkas/artifacts';
1
+ import { TestHarness } from './harness.js';
2
+ export { HarnessConfig, SendResult, clearMassRecords, createTestHarness, disableMassTracking, enableMassTracking, getMassRecords } from './harness.js';
3
+ export { ReproducibilityReport, generateReproducibilityReport } from './reproducibility.js';
4
+ export { AdversarialFixtures } from './adversarial-fixtures.js';
5
+ import '@hardkas/localnet';
4
6
 
5
- /**
6
- * System Invariant Verifiers
7
- *
8
- * Instead of just unit tests, these provide "System Guarantees".
9
- */
10
- declare const Invariants: {
11
- /**
12
- * Guarantee: An artifact's content hash MUST match its current state.
13
- */
14
- verifyArtifactIntegrity(artifact: BaseArtifact<any>, calculateHash: (a: any) => string): Promise<boolean>;
15
- /**
16
- * Guarantee: A Lineage chain MUST be strictly monotonic and linked.
17
- */
18
- verifyLineageContinuity(artifacts: BaseArtifact<any>[]): boolean;
19
- /**
20
- * Guarantee: Every emission to the event bus MUST have a timestamp.
21
- */
22
- verifyEventCompliance(event: CoreEvent): boolean;
23
- };
24
- /**
25
- * Invariant Watcher
26
- * Can be hooked into the event bus to detect violations in real-time.
27
- */
28
- declare class InvariantWatcher {
29
- private violations;
30
- constructor();
31
- getViolations(): string[];
32
- hasViolations(): boolean;
33
- }
34
-
35
- interface FuzzResult {
36
- ok: boolean;
37
- iterations: number;
38
- violations: string[];
7
+ interface FixtureDefinition {
8
+ name: string;
9
+ accounts?: number;
10
+ initialBalance?: bigint;
11
+ /** Pre-run transactions to set up state. */
12
+ setup?: Array<{
13
+ from: string;
14
+ to: string;
15
+ amountSompi: bigint;
16
+ }>;
39
17
  }
40
18
  /**
41
- * Custom Scenario Fuzzer for UTXO Invariants.
42
- * Verifies that sum(inputs) == sum(outputs) + fee across random transaction sequences.
19
+ * Create a fixture a pre-configured harness with setup transactions already applied.
43
20
  */
44
- declare function runUtxoFuzzer(iterations?: number): Promise<FuzzResult>;
21
+ declare function createFixture(def: FixtureDefinition): TestHarness;
45
22
 
46
- /**
47
- * HardKAS Testing Runtime
48
- */
49
- interface HardkasTestRuntime {
50
- readonly hardkas: Hardkas;
51
- readonly network: NetworkId;
52
- readonly accounts: Hardkas["accounts"];
53
- readonly tx: Hardkas["tx"];
54
- readonly localnet: Hardkas["localnet"];
55
- readonly query: Hardkas["query"];
23
+ interface HardKasMatchers<R = void> {
24
+ /** Assert a receipt has status "accepted". */
25
+ toBeAccepted(): R;
26
+ /** Assert a receipt has status "failed". */
27
+ toBeFailed(): R;
28
+ /** Assert a receipt contains a valid txId (starts with "simtx_" or is 64-char hex). */
29
+ toHaveValidTxId(): R;
30
+ /** Assert an artifact has a valid contentHash (64-char hex). */
31
+ toHaveValidContentHash(): R;
32
+ /** Assert an artifact passes lineage verification. */
33
+ toPassLineageCheck(): R;
34
+ /** Assert a GHOSTDAG result colors this block as blue. */
35
+ toBeBlueBlock(): R;
36
+ /** Assert a GHOSTDAG result colors this block as red. */
37
+ toBeRedBlock(): R;
38
+ /** Assert a balance (bigint sompi) increased by at least `amount`. */
39
+ toHaveIncreasedBy(amount: bigint): R;
40
+ /** Assert a balance (bigint sompi) decreased by at least `amount`. */
41
+ toHaveDecreasedBy(amount: bigint): R;
42
+ /** Assert a simulated DAG has no red blocks. */
43
+ toHaveNoRedBlocks(): R;
44
+ /** Assert a simulated DAG has exactly N tips. */
45
+ toHaveDagWidth(width: number): R;
56
46
  }
57
- interface HardkasTestOptions {
58
- cwd?: string;
59
- network?: NetworkId;
60
- autoStartLocalnet?: boolean;
61
- resetBetweenTests?: boolean;
47
+ declare module "vitest" {
48
+ interface Assertion<T = any> extends HardKasMatchers<T> {
49
+ }
50
+ interface AsymmetricMatchersContaining extends HardKasMatchers {
51
+ }
62
52
  }
63
- /**
64
- * The main helper for HardKAS tests.
65
- * Injects hooks for Vitest and provides access to the SDK.
66
- */
67
- declare function hardkasTest(options?: HardkasTestOptions): HardkasTestRuntime;
53
+ declare const hardKasMatchers: {
54
+ toBeAccepted(received: any): {
55
+ pass: boolean;
56
+ message: () => string;
57
+ };
58
+ toBeFailed(received: any): {
59
+ pass: boolean;
60
+ message: () => string;
61
+ };
62
+ toHaveValidTxId(received: any): {
63
+ pass: boolean;
64
+ message: () => string;
65
+ };
66
+ toHaveValidContentHash(received: any): {
67
+ pass: boolean;
68
+ message: () => string;
69
+ };
70
+ toPassLineageCheck(received: any): {
71
+ pass: boolean;
72
+ message: () => "Expected artifact NOT to pass lineage check" | "Expected artifact to pass lineage check (missing or empty lineage)";
73
+ };
74
+ toBeBlueBlock(received: any): {
75
+ pass: boolean;
76
+ message: () => "Expected block NOT to be blue, but it is" | "Expected block to be blue, but it is red or unknown";
77
+ };
78
+ toBeRedBlock(received: any): {
79
+ pass: boolean;
80
+ message: () => "Expected block NOT to be red, but it is" | "Expected block to be red, but it is blue or unknown";
81
+ };
82
+ toHaveIncreasedBy(received: any, amount: bigint): {
83
+ pass: boolean;
84
+ message: () => string;
85
+ };
86
+ toHaveDecreasedBy(received: any, amount: bigint): {
87
+ pass: boolean;
88
+ message: () => string;
89
+ };
90
+ toHaveNoRedBlocks(received: any): {
91
+ pass: boolean;
92
+ message: () => string;
93
+ };
94
+ toHaveDagWidth(received: any, width: number): {
95
+ pass: boolean;
96
+ message: () => string;
97
+ };
98
+ };
68
99
 
69
- export { type FuzzResult, type HardkasTestOptions, type HardkasTestRuntime, InvariantWatcher, Invariants, hardkasTest, runUtxoFuzzer };
100
+ export { type FixtureDefinition, type HardKasMatchers, TestHarness, createFixture, hardKasMatchers };
package/dist/index.js CHANGED
@@ -1,170 +1,45 @@
1
- // src/index.ts
2
- import { Hardkas } from "@hardkas/sdk";
3
- import { beforeAll, beforeEach } from "vitest";
1
+ import {
2
+ AdversarialFixtures
3
+ } from "./chunk-3EK7ATR5.js";
4
+ import {
5
+ generateReproducibilityReport
6
+ } from "./chunk-2HYIC4UP.js";
7
+ import {
8
+ clearMassRecords,
9
+ createTestHarness,
10
+ disableMassTracking,
11
+ enableMassTracking,
12
+ getMassRecords
13
+ } from "./chunk-CUJL53GG.js";
14
+ import {
15
+ hardKasMatchers
16
+ } from "./chunk-UHH25II3.js";
4
17
 
5
- // src/invariants.ts
6
- import { coreEvents } from "@hardkas/core";
7
- var Invariants = {
8
- /**
9
- * Guarantee: An artifact's content hash MUST match its current state.
10
- */
11
- async verifyArtifactIntegrity(artifact, calculateHash) {
12
- if (!artifact.contentHash) return true;
13
- const actual = calculateHash(artifact);
14
- return artifact.contentHash === actual;
15
- },
16
- /**
17
- * Guarantee: A Lineage chain MUST be strictly monotonic and linked.
18
- */
19
- verifyLineageContinuity(artifacts) {
20
- if (artifacts.length < 2) return true;
21
- const sorted = [...artifacts].sort(
22
- (a, b) => (a.lineage?.sequence ?? 0) - (b.lineage?.sequence ?? 0)
23
- );
24
- for (let i = 1; i < sorted.length; i++) {
25
- const prev = sorted[i - 1];
26
- const curr = sorted[i];
27
- if (!prev || !curr) continue;
28
- if (curr.lineage?.parentArtifactId !== prev.lineage?.artifactId) {
29
- return false;
30
- }
31
- if (curr.lineage?.lineageId !== prev.lineage?.lineageId) {
32
- return false;
33
- }
34
- }
35
- return true;
36
- },
37
- /**
38
- * Guarantee: Every emission to the event bus MUST have a timestamp.
39
- */
40
- verifyEventCompliance(event) {
41
- return !!event.timestamp;
42
- }
43
- };
44
- var InvariantWatcher = class {
45
- violations = [];
46
- constructor() {
47
- coreEvents.on((event) => {
48
- if (!Invariants.verifyEventCompliance(event)) {
49
- this.violations.push(`Event compliance violation: ${event.kind}`);
50
- }
51
- if (event.kind === "integrity.hash_mismatch") {
52
- const payload = event.payload;
53
- this.violations.push(`Hash mismatch detected: ${payload.artifactId || event.artifactId}`);
54
- }
55
- });
56
- }
57
- getViolations() {
58
- return this.violations;
59
- }
60
- hasViolations() {
61
- return this.violations.length > 0;
62
- }
63
- };
64
-
65
- // src/utxo-fuzzer.ts
66
- import { SOMPI_PER_KAS } from "@hardkas/core";
67
- import { buildPaymentPlan } from "@hardkas/tx-builder";
68
- import { applySimulatedPayment, createInitialLocalnetState } from "@hardkas/localnet";
69
- async function runUtxoFuzzer(iterations = 50) {
70
- let state = createInitialLocalnetState({ accounts: 5, initialBalanceSompi: 1000n * SOMPI_PER_KAS });
71
- const violations = [];
72
- for (let i = 0; i < iterations; i++) {
73
- const fromIdx = Math.floor(Math.random() * state.accounts.length);
74
- let toIdx = Math.floor(Math.random() * state.accounts.length);
75
- if (fromIdx === toIdx) toIdx = (toIdx + 1) % state.accounts.length;
76
- const fromAccount = state.accounts[fromIdx];
77
- const toAccount = state.accounts[toIdx];
78
- const amountSompi = BigInt(Math.floor(Math.random() * 10)) * SOMPI_PER_KAS + BigInt(Math.floor(Math.random() * 1e6));
79
- try {
80
- const unspent = state.utxos.filter((u) => u.address === fromAccount.address && !u.spent);
81
- if (unspent.length === 0) continue;
82
- const builderUtxos = unspent.map((u) => ({
83
- outpoint: { transactionId: u.id.split(":")[0], index: 0 },
84
- address: u.address,
85
- amountSompi: BigInt(u.amountSompi),
86
- scriptPublicKey: "mock"
87
- }));
88
- const plan = buildPaymentPlan({
89
- fromAddress: fromAccount.address,
90
- availableUtxos: builderUtxos,
91
- outputs: [{ address: toAccount.address, amountSompi }],
92
- feeRateSompiPerMass: 1n
93
- });
94
- const inputSum = plan.inputs.reduce((s, x) => s + x.amountSompi, 0n);
95
- const outputSum = plan.outputs.reduce((s, x) => s + x.amountSompi, 0n) + (plan.change?.amountSompi || 0n);
96
- const fee = plan.estimatedFeeSompi;
97
- if (inputSum !== outputSum + fee) {
98
- violations.push(`Iteration ${i}: Planning Invariant Violated! ${inputSum} != ${outputSum} + ${fee}`);
99
- }
100
- const result = applySimulatedPayment(state, {
101
- from: fromAccount.name,
102
- to: toAccount.name,
103
- amountSompi
104
- });
105
- state = result.state;
106
- const totalInState = state.utxos.filter((u) => !u.spent).reduce((s, x) => s + BigInt(x.amountSompi), 0n);
107
- const expectedTotal = BigInt(state.accounts.length) * 1000n * SOMPI_PER_KAS - BigInt(i + 1) * fee;
108
- const utxoIds = state.utxos.map((u) => u.id);
109
- const uniqueIds = new Set(utxoIds);
110
- if (utxoIds.length !== uniqueIds.size) {
111
- violations.push(`Iteration ${i}: Duplicate UTXO IDs detected in state!`);
112
- }
113
- } catch (e) {
114
- if (!e.message.includes("Insufficient funds")) {
115
- violations.push(`Iteration ${i}: Unexpected Error: ${e.message}`);
18
+ // src/fixtures.ts
19
+ function createFixture(def) {
20
+ const config = {
21
+ accounts: def.accounts,
22
+ initialBalance: def.initialBalance
23
+ };
24
+ const harness = createTestHarness(config);
25
+ if (def.setup) {
26
+ for (const tx of def.setup) {
27
+ const result = harness.send(tx);
28
+ if (!result.ok) {
29
+ throw new Error(`Fixture "${def.name}" failed during setup: ${tx.from} -> ${tx.to} (${tx.amountSompi} sompi). Error: ${result.receipt?.errors?.join(", ") || "Unknown error"}`);
116
30
  }
117
31
  }
118
32
  }
119
- return {
120
- ok: violations.length === 0,
121
- iterations,
122
- violations
123
- };
124
- }
125
-
126
- // src/index.ts
127
- function hardkasTest(options = {}) {
128
- const cwd = options.cwd || process.env.HARDKAS_CWD || ".";
129
- const network = options.network || process.env.HARDKAS_NETWORK || "simnet";
130
- const autoStart = options.autoStartLocalnet ?? true;
131
- const autoReset = options.resetBetweenTests ?? true;
132
- let sdk;
133
- beforeAll(async () => {
134
- sdk = await Hardkas.open(cwd);
135
- if (autoStart && network === "simnet") {
136
- await sdk.localnet.start();
137
- }
138
- });
139
- beforeEach(async () => {
140
- if (autoReset && network === "simnet") {
141
- await sdk.localnet.reset();
142
- }
143
- });
144
- return {
145
- get hardkas() {
146
- return sdk;
147
- },
148
- get network() {
149
- return network;
150
- },
151
- get accounts() {
152
- return sdk.accounts;
153
- },
154
- get tx() {
155
- return sdk.tx;
156
- },
157
- get localnet() {
158
- return sdk.localnet;
159
- },
160
- get query() {
161
- return sdk.query;
162
- }
163
- };
33
+ return harness;
164
34
  }
165
35
  export {
166
- InvariantWatcher,
167
- Invariants,
168
- hardkasTest,
169
- runUtxoFuzzer
36
+ AdversarialFixtures,
37
+ clearMassRecords,
38
+ createFixture,
39
+ createTestHarness,
40
+ disableMassTracking,
41
+ enableMassTracking,
42
+ generateReproducibilityReport,
43
+ getMassRecords,
44
+ hardKasMatchers
170
45
  };
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,8 @@
1
+ import {
2
+ enableMassTracking
3
+ } from "./chunk-CUJL53GG.js";
4
+
5
+ // src/mass-setup.ts
6
+ if (process.env.HARDKAS_MASS_TRACKING === "1") {
7
+ enableMassTracking();
8
+ }
@@ -0,0 +1,24 @@
1
+ interface ReproducibilityReport {
2
+ /** Fixed proof version — does NOT change between releases. */
3
+ proofVersion: "repro-v0";
4
+ /** Informational only — NOT part of hash comparison. */
5
+ hardkasVersion: string;
6
+ artifacts: {
7
+ l1Plan: string;
8
+ l1Signed: string;
9
+ igraPlan: string;
10
+ dagLinearScenario: string;
11
+ dagWideScenario: string;
12
+ massProfile: string;
13
+ canonicalNested: string;
14
+ simulatedTxReceipt: string;
15
+ };
16
+ }
17
+ /**
18
+ * Generate the reproducibility report.
19
+ * This function MUST be fully deterministic.
20
+ * Same code version → same output → always → everywhere.
21
+ */
22
+ declare function generateReproducibilityReport(): ReproducibilityReport;
23
+
24
+ export { type ReproducibilityReport, generateReproducibilityReport };
@@ -0,0 +1,7 @@
1
+ import {
2
+ generateReproducibilityReport
3
+ } from "./chunk-2HYIC4UP.js";
4
+ import "./chunk-CUJL53GG.js";
5
+ export {
6
+ generateReproducibilityReport
7
+ };
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/setup.js ADDED
@@ -0,0 +1 @@
1
+ import "./chunk-UHH25II3.js";
package/package.json CHANGED
@@ -1,20 +1,25 @@
1
1
  {
2
2
  "name": "@hardkas/testing",
3
- "version": "0.2.2-alpha.1",
3
+ "version": "0.4.0-alpha",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
7
7
  "exports": {
8
- ".": "./dist/index.js"
8
+ ".": "./dist/index.js",
9
+ "./harness": "./dist/harness.js",
10
+ "./setup": "./dist/setup.js",
11
+ "./adversarial-fixtures": "./dist/adversarial-fixtures.js",
12
+ "./mass-setup": "./dist/mass-setup.js"
9
13
  },
10
14
  "dependencies": {
11
- "@hardkas/artifacts": "0.2.2-alpha.1",
12
- "@hardkas/sdk": "0.2.2-alpha.1",
13
- "@hardkas/kaspa-rpc": "0.2.2-alpha.1",
14
- "@hardkas/core": "0.2.2-alpha.1",
15
- "@hardkas/localnet": "0.2.2-alpha.1",
16
- "@hardkas/simulator": "0.2.2-alpha.1",
17
- "@hardkas/tx-builder": "0.2.2-alpha.1"
15
+ "@hardkas/artifacts": "0.4.0-alpha",
16
+ "@hardkas/core": "0.4.0-alpha",
17
+ "@hardkas/localnet": "0.4.0-alpha",
18
+ "@hardkas/kaspa-rpc": "0.4.0-alpha",
19
+ "@hardkas/query-store": "0.4.0-alpha",
20
+ "@hardkas/sdk": "0.4.0-alpha",
21
+ "@hardkas/simulator": "0.4.0-alpha",
22
+ "@hardkas/tx-builder": "0.4.0-alpha"
18
23
  },
19
24
  "devDependencies": {
20
25
  "tsup": "^8.3.5",
@@ -38,8 +43,9 @@
38
43
  "README.md"
39
44
  ],
40
45
  "scripts": {
41
- "build": "tsup src/index.ts --format esm --dts --clean --external vitest",
42
- "test": "vitest run",
46
+ "build": "tsup src/index.ts src/harness.ts src/setup.ts src/reproducibility.ts src/adversarial-fixtures.ts src/mass-setup.ts --format esm --dts --clean --external vitest",
47
+ "test": "vitest run && pnpm test:gauntlet",
48
+ "test:gauntlet": "node --import tsx --test test/gauntlet.node.ts",
43
49
  "typecheck": "tsc --noEmit",
44
50
  "lint": "eslint ."
45
51
  }