Summary
The test suite currently has no coverage of the critical paths that matter most: the multi-step deployContract() pipeline, the Soroban simulation flow, the full compile-to-WASM codegen pipeline, and the graph persistence/import/export layer. This issue defines and implements a comprehensive test coverage plan across all four layers with mock strategies, edge case coverage, and a coverage gate enforced in CI.
Why This Is Hard
- Mocking the Stellar SDK:
deployContract() calls SorobanRpc.Server, Horizon.Server, TransactionBuilder, and the Freighter API across three async stages. Setting up correct mocks that simulate both the happy path and the five failure modes (wallet rejection, simulation error, upload collision, tx poll timeout, chain failure) requires careful dependency injection design.
- Codegen correctness is combinatorial:
generateContractSource has 6 block types × 3 param combinations × 2 execution order variants. Testing all meaningful combinations without writing 50 copy-paste test cases requires parameterised test factories.
- The compile/validate pipeline has subtle invariants:
validateContractGraph has 5 validation passes. Each pass can fail independently or in combination. Tests must cover single-pass failures, multi-pass failures, and valid graphs that must not regress.
- Coverage gate: the CI pipeline must fail if coverage drops below 80% on the
src/lib/compile/ and src/lib/stellar/ directories. Setting this up correctly in Vitest requires per-directory thresholds in vitest.config.ts, which is non-trivial configuration.
- Test isolation:
localStorage is shared across tests. Graph persistence tests must reset state in beforeEach to avoid flaky cross-test contamination.
Test Plan by Module
src/lib/stellar/deploy.ts
| Test |
Scenario |
connectWallet returns public key on success |
Happy path |
connectWallet throws on extension rejection |
User denies |
connectWallet throws on missing extension |
Not installed |
fetchWalletBalance returns native XLM balance |
Happy path |
fetchWalletBalance throws on missing native balance |
No XLM account |
compileContract sends correct payload and returns wasm |
Happy path |
compileContract throws CompileContractError on API error |
Compile fail |
deployContract calls wallet → compile → upload → instantiate in order |
Full pipeline |
deployContract skips upload when "ExistingValue" error |
WASM already uploaded |
deployContract returns contract ID from createSim retval |
ID from simulation |
deployContract falls back to hash-derived ID when retval absent |
Fallback ID |
src/lib/stellar/simulate.ts
| Test |
Scenario |
inferArgsFromGraph returns correct args for Transfer graph |
Transfer nodes |
inferArgsFromGraph returns correct args for Storage+Condition graph |
Storage/Condition |
inferArgsFromGraph returns input fallback for empty graph |
No typed nodes |
simulateContract serialises graph and returns result |
Happy path |
src/lib/compile/codegen.ts
| Test |
Scenario |
generateContractSource emits require_auth for Auth node |
Auth block |
generateContractSource emits token::Client::new for Transfer node |
Transfer block |
generateContractSource emits env.storage().instance().set for Storage node |
Storage write |
generateContractSource emits env.events().publish for Event node |
Event block |
generateContractSource emits structured if !() for Condition with expression |
Condition block |
generateContractSource emits legacy if !release for Condition without expression |
Condition legacy |
generateContractSource includes Address in imports when Auth present |
Import derivation |
generateContractSource includes token in imports when Transfer present |
Import derivation |
getExecutionOrder returns nodes in BFS order from Start |
Ordering |
getExecutionOrder skips unreachable nodes |
Reachability |
src/lib/compile/validate.ts
| Test |
Scenario |
validateContractGraph rejects payload over MAX_GRAPH_BYTES |
Size limit |
validateContractGraph rejects missing nodes array |
Invalid payload |
validateContractGraph rejects duplicate node IDs |
Duplicate IDs |
validateContractGraph rejects edge referencing unknown node |
Dangling edge |
validateContractGraph rejects graph with no Start node |
Missing start |
validateContractGraph rejects graph with two Start nodes |
Multiple starts |
validateGraphStructure rejects graph with no reachable executable blocks |
No logic |
validateConditionExpression rejects invalid operator |
Bad operator |
validateConditionExpression rejects empty left operand |
Incomplete expr |
src/lib/editor/graphPersistence.ts
| Test |
Scenario |
saveGraphToStorage + loadGraphFromStorage round-trip |
Persist & restore |
loadGraphFromStorage returns null when storage empty |
Cold start |
parseImportedGraphJson accepts valid graph JSON |
Import success |
parseImportedGraphJson rejects malformed JSON |
Import fail |
downloadGraphJson triggers browser download with correct filename |
Export |
Coverage Gate
vitest.config.ts must set:
coverage: {
thresholds: {
"src/lib/compile/": { lines: 80, functions: 80 },
"src/lib/stellar/": { lines: 80, functions: 80 },
"src/lib/editor/": { lines: 75, functions: 75 },
}
}
CI fails if any threshold is not met.
Acceptance Criteria
Summary
The test suite currently has no coverage of the critical paths that matter most: the multi-step
deployContract()pipeline, the Soroban simulation flow, the full compile-to-WASM codegen pipeline, and the graph persistence/import/export layer. This issue defines and implements a comprehensive test coverage plan across all four layers with mock strategies, edge case coverage, and a coverage gate enforced in CI.Why This Is Hard
deployContract()callsSorobanRpc.Server,Horizon.Server,TransactionBuilder, and the Freighter API across three async stages. Setting up correct mocks that simulate both the happy path and the five failure modes (wallet rejection, simulation error, upload collision, tx poll timeout, chain failure) requires careful dependency injection design.generateContractSourcehas 6 block types × 3 param combinations × 2 execution order variants. Testing all meaningful combinations without writing 50 copy-paste test cases requires parameterised test factories.validateContractGraphhas 5 validation passes. Each pass can fail independently or in combination. Tests must cover single-pass failures, multi-pass failures, and valid graphs that must not regress.src/lib/compile/andsrc/lib/stellar/directories. Setting this up correctly in Vitest requires per-directory thresholds invitest.config.ts, which is non-trivial configuration.localStorageis shared across tests. Graph persistence tests must reset state inbeforeEachto avoid flaky cross-test contamination.Test Plan by Module
src/lib/stellar/deploy.tsconnectWalletreturns public key on successconnectWalletthrows on extension rejectionconnectWalletthrows on missing extensionfetchWalletBalancereturns native XLM balancefetchWalletBalancethrows on missing native balancecompileContractsends correct payload and returns wasmcompileContractthrowsCompileContractErroron API errordeployContractcalls wallet → compile → upload → instantiate in orderdeployContractskips upload when "ExistingValue" errordeployContractreturns contract ID fromcreateSimretvaldeployContractfalls back to hash-derived ID when retval absentsrc/lib/stellar/simulate.tsinferArgsFromGraphreturns correct args for Transfer graphinferArgsFromGraphreturns correct args for Storage+Condition graphinferArgsFromGraphreturnsinputfallback for empty graphsimulateContractserialises graph and returns resultsrc/lib/compile/codegen.tsgenerateContractSourceemitsrequire_authfor Auth nodegenerateContractSourceemitstoken::Client::newfor Transfer nodegenerateContractSourceemitsenv.storage().instance().setfor Storage nodegenerateContractSourceemitsenv.events().publishfor Event nodegenerateContractSourceemits structuredif !()for Condition with expressiongenerateContractSourceemits legacyif !releasefor Condition without expressiongenerateContractSourceincludesAddressin imports when Auth presentgenerateContractSourceincludestokenin imports when Transfer presentgetExecutionOrderreturns nodes in BFS order from StartgetExecutionOrderskips unreachable nodessrc/lib/compile/validate.tsvalidateContractGraphrejects payload overMAX_GRAPH_BYTESvalidateContractGraphrejects missing nodes arrayvalidateContractGraphrejects duplicate node IDsvalidateContractGraphrejects edge referencing unknown nodevalidateContractGraphrejects graph with no Start nodevalidateContractGraphrejects graph with two Start nodesvalidateGraphStructurerejects graph with no reachable executable blocksvalidateConditionExpressionrejects invalid operatorvalidateConditionExpressionrejects empty left operandsrc/lib/editor/graphPersistence.tssaveGraphToStorage+loadGraphFromStorageround-triploadGraphFromStoragereturns null when storage emptyparseImportedGraphJsonaccepts valid graph JSONparseImportedGraphJsonrejects malformed JSONdownloadGraphJsontriggers browser download with correct filenameCoverage Gate
vitest.config.tsmust set:CI fails if any threshold is not met.
Acceptance Criteria
vi.mock("@stellar/freighter-api", ...)is used for all wallet-dependent tests — no real extension calls.SorobanRpc.ServerandHorizon.Serverare mocked viavi.mock("@stellar/stellar-sdk", ...).localStorageis cleared inbeforeEachfor all graph persistence tests.vitest.config.tsand enforced in CI.npm run test:coverageruns and produces a coverage report.setTimeoutwith real delays — async resolution is mocked.