@hardkas/testing 0.2.2-alpha → 0.3.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,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,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.3.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,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,25 +1,100 @@
1
- import { MockKaspaRpcClient } from '@hardkas/kaspa-rpc';
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';
2
6
 
3
- interface TestWallet {
4
- readonly name: string;
5
- readonly address: 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
+ }>;
6
17
  }
7
- interface HardkasTestContext {
8
- readonly wallets: {
9
- readonly alice: TestWallet;
10
- readonly bob: TestWallet;
11
- readonly carol: TestWallet;
12
- readonly faucet: TestWallet;
13
- };
14
- readonly rpc: MockKaspaRpcClient;
15
- readonly faucet: {
16
- fund(address: string, amountSompi: bigint): Promise<void>;
17
- };
18
- reset(): Promise<void>;
18
+ /**
19
+ * Create a fixture — a pre-configured harness with setup transactions already applied.
20
+ */
21
+ declare function createFixture(def: FixtureDefinition): TestHarness;
22
+
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;
46
+ }
47
+ declare module "vitest" {
48
+ interface Assertion<T = any> extends HardKasMatchers<T> {
49
+ }
50
+ interface AsymmetricMatchersContaining extends HardKasMatchers {
51
+ }
19
52
  }
20
- declare function createHardkasTestContext(): Promise<HardkasTestContext>;
21
- declare const hardkas: {
22
- localnet: typeof createHardkasTestContext;
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
+ };
23
98
  };
24
99
 
25
- export { type HardkasTestContext, type TestWallet, createHardkasTestContext, hardkas };
100
+ export { type FixtureDefinition, type HardKasMatchers, TestHarness, createFixture, hardKasMatchers };
package/dist/index.js CHANGED
@@ -1,50 +1,45 @@
1
- // src/index.ts
2
- import { MockKaspaRpcClient } from "@hardkas/kaspa-rpc";
3
- import { createDeterministicAccounts } from "@hardkas/localnet";
4
- import { createMockUtxo } from "@hardkas/tx-builder";
5
- async function createHardkasTestContext() {
6
- const rpc = new MockKaspaRpcClient("simnet");
7
- const accounts = createDeterministicAccounts({
8
- count: 4,
9
- initialBalanceSompi: 0n
10
- });
11
- const alice = accounts[0];
12
- const bob = accounts[1];
13
- const carol = accounts[2];
14
- if (!alice || !bob || !carol) {
15
- throw new Error("Failed to create deterministic test accounts.");
16
- }
17
- const wallets = {
18
- alice: { name: alice.name, address: alice.address },
19
- bob: { name: bob.name, address: bob.address },
20
- carol: { name: carol.name, address: carol.address },
21
- faucet: { name: "faucet", address: "kaspa:sim_faucet" }
1
+ import {
2
+ AdversarialFixtures
3
+ } from "./chunk-3EK7ATR5.js";
4
+ import {
5
+ generateReproducibilityReport
6
+ } from "./chunk-WIN7YDBM.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";
17
+
18
+ // src/fixtures.ts
19
+ function createFixture(def) {
20
+ const config = {
21
+ accounts: def.accounts,
22
+ initialBalance: def.initialBalance
22
23
  };
23
- return {
24
- wallets,
25
- rpc,
26
- faucet: {
27
- async fund(address, amountSompi) {
28
- rpc.setUtxos(address, [
29
- createMockUtxo({
30
- address,
31
- amountSompi,
32
- index: 0
33
- })
34
- ]);
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"}`);
35
30
  }
36
- },
37
- async reset() {
38
- rpc.setUtxos(wallets.alice.address, []);
39
- rpc.setUtxos(wallets.bob.address, []);
40
- rpc.setUtxos(wallets.carol.address, []);
41
31
  }
42
- };
32
+ }
33
+ return harness;
43
34
  }
44
- var hardkas = {
45
- localnet: createHardkasTestContext
46
- };
47
35
  export {
48
- createHardkasTestContext,
49
- hardkas
36
+ AdversarialFixtures,
37
+ clearMassRecords,
38
+ createFixture,
39
+ createTestHarness,
40
+ disableMassTracking,
41
+ enableMassTracking,
42
+ generateReproducibilityReport,
43
+ getMassRecords,
44
+ hardKasMatchers
50
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-WIN7YDBM.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,19 +1,25 @@
1
1
  {
2
2
  "name": "@hardkas/testing",
3
- "version": "0.2.2-alpha",
3
+ "version": "0.3.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",
12
- "@hardkas/core": "0.2.2-alpha",
13
- "@hardkas/kaspa-rpc": "0.2.2-alpha",
14
- "@hardkas/simulator": "0.2.2-alpha",
15
- "@hardkas/localnet": "0.2.2-alpha",
16
- "@hardkas/tx-builder": "0.2.2-alpha"
15
+ "@hardkas/artifacts": "0.3.0-alpha",
16
+ "@hardkas/kaspa-rpc": "0.3.0-alpha",
17
+ "@hardkas/localnet": "0.3.0-alpha",
18
+ "@hardkas/core": "0.3.0-alpha",
19
+ "@hardkas/tx-builder": "0.3.0-alpha",
20
+ "@hardkas/query-store": "0.3.0-alpha",
21
+ "@hardkas/simulator": "0.3.0-alpha",
22
+ "@hardkas/sdk": "0.3.0-alpha"
17
23
  },
18
24
  "devDependencies": {
19
25
  "tsup": "^8.3.5",
@@ -37,8 +43,9 @@
37
43
  "README.md"
38
44
  ],
39
45
  "scripts": {
40
- "build": "tsup src/index.ts --format esm --dts --clean",
41
- "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",
42
49
  "typecheck": "tsc --noEmit",
43
50
  "lint": "eslint ."
44
51
  }