diff --git a/.github/workflows/pull-request-main.yml b/.github/workflows/pull-request-main.yml
index db42d6be..e115d4f5 100644
--- a/.github/workflows/pull-request-main.yml
+++ b/.github/workflows/pull-request-main.yml
@@ -47,6 +47,11 @@ jobs:
contents: read
actions: read
steps:
+ - name: Install Bun
+ run: |
+ curl -fsSL https://bun.sh/install | bash
+ echo "$HOME/.bun/bin" >> "$GITHUB_PATH"
+
- name: ci-test
uses: smartcontractkit/.github/actions/ci-test-go@ci-test-go/0.3.5
with:
diff --git a/cmd/creinit/creinit_test.go b/cmd/creinit/creinit_test.go
index f414b1b5..a71cedaa 100644
--- a/cmd/creinit/creinit_test.go
+++ b/cmd/creinit/creinit_test.go
@@ -3,6 +3,7 @@ package creinit
import (
"fmt"
"os"
+ "os/exec"
"path/filepath"
"testing"
@@ -17,7 +18,9 @@ func GetTemplateFileListGo() []string {
return []string{
"README.md",
"main.go",
+ "workflow.go",
"workflow.yaml",
+ "workflow_test.go",
}
}
@@ -75,6 +78,54 @@ func requireNoDirExists(t *testing.T, dirPath string) {
require.Falsef(t, fi.IsDir(), "directory %s should NOT exist", dirPath)
}
+// runLanguageSpecificTests runs the appropriate test suite based on the language field.
+// For TypeScript: runs bun install and bun test in the workflow directory.
+// For Go: runs go test ./... in the workflow directory.
+func runLanguageSpecificTests(t *testing.T, workflowDir, language string) {
+ t.Helper()
+
+ switch language {
+ case "typescript":
+ runTypescriptTests(t, workflowDir)
+ case "go":
+ runGoTests(t, workflowDir)
+ default:
+ t.Logf("Unknown language %q, skipping tests", language)
+ }
+}
+
+// runTypescriptTests executes TypeScript tests using bun.
+// Follows the cre init instructions: bun install --cwd
then bun test in that directory.
+func runTypescriptTests(t *testing.T, workflowDir string) {
+ t.Helper()
+
+ t.Logf("Running TypeScript tests in %s", workflowDir)
+ installCmd := exec.Command("bun", "install", "--cwd", workflowDir, "--ignore-scripts")
+ installOutput, err := installCmd.CombinedOutput()
+ require.NoError(t, err, "bun install failed in %s:\n%s", workflowDir, string(installOutput))
+ t.Logf("bun install succeeded")
+
+ // Run tests
+ testCmd := exec.Command("bun", "test")
+ testCmd.Dir = workflowDir
+ testOutput, err := testCmd.CombinedOutput()
+ require.NoError(t, err, "bun test failed in %s:\n%s", workflowDir, string(testOutput))
+ t.Logf("bun test passed:\n%s", string(testOutput))
+}
+
+// runGoTests executes Go tests in the workflow directory.
+func runGoTests(t *testing.T, workflowDir string) {
+ t.Helper()
+
+ t.Logf("Running Go tests in %s", workflowDir)
+
+ testCmd := exec.Command("go", "test", "./...")
+ testCmd.Dir = workflowDir
+ testOutput, err := testCmd.CombinedOutput()
+ require.NoError(t, err, "go test failed in %s:\n%s", workflowDir, string(testOutput))
+ t.Logf("go test passed:\n%s", string(testOutput))
+}
+
func TestInitExecuteFlows(t *testing.T) {
// All inputs are provided via flags to avoid interactive prompts
cases := []struct {
@@ -86,6 +137,7 @@ func TestInitExecuteFlows(t *testing.T) {
expectProjectDirRel string
expectWorkflowName string
expectTemplateFiles []string
+ language string // "go" or "typescript"
}{
{
name: "Go PoR template with all flags",
@@ -96,6 +148,7 @@ func TestInitExecuteFlows(t *testing.T) {
expectProjectDirRel: "myproj",
expectWorkflowName: "myworkflow",
expectTemplateFiles: GetTemplateFileListGo(),
+ language: "go",
},
{
name: "Go HelloWorld template with all flags",
@@ -106,6 +159,7 @@ func TestInitExecuteFlows(t *testing.T) {
expectProjectDirRel: "alpha",
expectWorkflowName: "default-wf",
expectTemplateFiles: GetTemplateFileListGo(),
+ language: "go",
},
{
name: "Go HelloWorld with different project name",
@@ -116,6 +170,7 @@ func TestInitExecuteFlows(t *testing.T) {
expectProjectDirRel: "projX",
expectWorkflowName: "workflow-X",
expectTemplateFiles: GetTemplateFileListGo(),
+ language: "go",
},
{
name: "Go PoR with workflow flag",
@@ -126,6 +181,7 @@ func TestInitExecuteFlows(t *testing.T) {
expectProjectDirRel: "projFlag",
expectWorkflowName: "flagged-wf",
expectTemplateFiles: GetTemplateFileListGo(),
+ language: "go",
},
{
name: "Go HelloWorld template by ID",
@@ -136,6 +192,7 @@ func TestInitExecuteFlows(t *testing.T) {
expectProjectDirRel: "tplProj",
expectWorkflowName: "workflow-Tpl",
expectTemplateFiles: GetTemplateFileListGo(),
+ language: "go",
},
{
name: "Go PoR template with rpc-url",
@@ -146,6 +203,7 @@ func TestInitExecuteFlows(t *testing.T) {
expectProjectDirRel: "porWithFlag",
expectWorkflowName: "por-wf-01",
expectTemplateFiles: GetTemplateFileListGo(),
+ language: "go",
},
{
name: "TS HelloWorld template with rpc-url (ignored)",
@@ -156,6 +214,18 @@ func TestInitExecuteFlows(t *testing.T) {
expectProjectDirRel: "tsWithRpcFlag",
expectWorkflowName: "ts-wf-flag",
expectTemplateFiles: GetTemplateFileListTS(),
+ language: "typescript",
+ },
+ {
+ name: "TS PoR template",
+ projectNameFlag: "tsPorProj",
+ templateIDFlag: 4, // TypeScript PoR
+ workflowNameFlag: "ts-por-wf",
+ rpcURLFlag: "https://sepolia.example/rpc",
+ expectProjectDirRel: "tsPorProj",
+ expectWorkflowName: "ts-por-wf",
+ expectTemplateFiles: GetTemplateFileListTS(),
+ language: "typescript",
},
}
@@ -184,8 +254,8 @@ func TestInitExecuteFlows(t *testing.T) {
projectRoot := filepath.Join(tempDir, tc.expectProjectDirRel)
validateInitProjectStructure(t, projectRoot, tc.expectWorkflowName, tc.expectTemplateFiles)
- // NOTE: We deliberately don't assert Go/TS scaffolding here because the
- // template chosen by prompt could vary; dedicated tests below cover both paths.
+
+ runLanguageSpecificTests(t, filepath.Join(projectRoot, tc.expectWorkflowName), tc.language)
})
}
}
diff --git a/cmd/creinit/template/workflow/blankTemplate/main.go.tpl b/cmd/creinit/template/workflow/blankTemplate/main.go.tpl
index 9b8dfb74..d30c0195 100644
--- a/cmd/creinit/template/workflow/blankTemplate/main.go.tpl
+++ b/cmd/creinit/template/workflow/blankTemplate/main.go.tpl
@@ -3,42 +3,10 @@
package main
import (
- "fmt"
- "log/slog"
-
- "github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron"
"github.com/smartcontractkit/cre-sdk-go/cre"
"github.com/smartcontractkit/cre-sdk-go/cre/wasm"
)
-type ExecutionResult struct {
- Result string
-}
-
-// Workflow configuration loaded from the config.json file
-type Config struct{}
-
-// Workflow implementation with a list of capability triggers
-func InitWorkflow(config *Config, logger *slog.Logger, secretsProvider cre.SecretsProvider) (cre.Workflow[*Config], error) {
- // Create the trigger
- cronTrigger := cron.Trigger(&cron.Config{Schedule: "*/30 * * * * *"}) // Fires every 30 seconds
-
- // Register a handler with the trigger and a callback function
- return cre.Workflow[*Config]{
- cre.Handler(cronTrigger, onCronTrigger),
- }, nil
-}
-
-func onCronTrigger(config *Config, runtime cre.Runtime, trigger *cron.Payload) (*ExecutionResult, error) {
- logger := runtime.Logger()
- scheduledTime := trigger.ScheduledExecutionTime.AsTime()
- logger.Info("Cron trigger fired", "scheduledTime", scheduledTime)
-
- // Your logic here...
-
- return &ExecutionResult{Result: fmt.Sprintf("Fired at %s", scheduledTime)}, nil
-}
-
func main() {
wasm.NewRunner(cre.ParseJSON[Config]).Run(InitWorkflow)
-}
\ No newline at end of file
+}
diff --git a/cmd/creinit/template/workflow/blankTemplate/workflow.go.tpl b/cmd/creinit/template/workflow/blankTemplate/workflow.go.tpl
new file mode 100644
index 00000000..97371950
--- /dev/null
+++ b/cmd/creinit/template/workflow/blankTemplate/workflow.go.tpl
@@ -0,0 +1,37 @@
+package main
+
+import (
+ "fmt"
+ "log/slog"
+
+ "github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron"
+ "github.com/smartcontractkit/cre-sdk-go/cre"
+)
+
+type ExecutionResult struct {
+ Result string
+}
+
+// Workflow configuration loaded from the config.json file
+type Config struct{}
+
+// Workflow implementation with a list of capability triggers
+func InitWorkflow(config *Config, logger *slog.Logger, secretsProvider cre.SecretsProvider) (cre.Workflow[*Config], error) {
+ // Create the trigger
+ cronTrigger := cron.Trigger(&cron.Config{Schedule: "*/30 * * * * *"}) // Fires every 30 seconds
+
+ // Register a handler with the trigger and a callback function
+ return cre.Workflow[*Config]{
+ cre.Handler(cronTrigger, onCronTrigger),
+ }, nil
+}
+
+func onCronTrigger(config *Config, runtime cre.Runtime, trigger *cron.Payload) (*ExecutionResult, error) {
+ logger := runtime.Logger()
+ scheduledTime := trigger.ScheduledExecutionTime.AsTime()
+ logger.Info("Cron trigger fired", "scheduledTime", scheduledTime)
+
+ // Your logic here...
+
+ return &ExecutionResult{Result: fmt.Sprintf("Fired at %s", scheduledTime)}, nil
+}
diff --git a/cmd/creinit/template/workflow/blankTemplate/workflow_test.go.tpl b/cmd/creinit/template/workflow/blankTemplate/workflow_test.go.tpl
new file mode 100644
index 00000000..472a42d7
--- /dev/null
+++ b/cmd/creinit/template/workflow/blankTemplate/workflow_test.go.tpl
@@ -0,0 +1,58 @@
+package main
+
+import (
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron"
+ "github.com/smartcontractkit/cre-sdk-go/cre/testutils"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/protobuf/types/known/timestamppb"
+)
+
+var anyExecutionTime = time.Date(2025, 7, 14, 17, 41, 57, 0, time.UTC)
+
+func TestInitWorkflow(t *testing.T) {
+ config := &Config{}
+ runtime := testutils.NewRuntime(t, testutils.Secrets{})
+
+ workflow, err := InitWorkflow(config, runtime.Logger(), nil)
+ require.NoError(t, err)
+
+ require.Len(t, workflow, 1)
+ require.Equal(t, cron.Trigger(&cron.Config{}).CapabilityID(), workflow[0].CapabilityID())
+}
+
+func TestOnCronTrigger(t *testing.T) {
+ config := &Config{}
+ runtime := testutils.NewRuntime(t, testutils.Secrets{})
+
+ payload := &cron.Payload{
+ ScheduledExecutionTime: timestamppb.New(anyExecutionTime),
+ }
+
+ result, err := onCronTrigger(config, runtime, payload)
+ require.NoError(t, err)
+ require.NotNil(t, result)
+ require.Contains(t, result.Result, "Fired at")
+ require.Contains(t, result.Result, "2025-07-14")
+
+ logs := runtime.GetLogs()
+ assertLogContains(t, logs, "Cron trigger fired")
+}
+
+func assertLogContains(t *testing.T, logs [][]byte, substr string) {
+ t.Helper()
+ for _, line := range logs {
+ if strings.Contains(string(line), substr) {
+ return
+ }
+ }
+ var logStrings []string
+ for _, log := range logs {
+ logStrings = append(logStrings, string(log))
+ }
+ t.Fatalf("Expected logs to contain substring %q, but it was not found in logs:\n%s",
+ substr, strings.Join(logStrings, "\n"))
+}
diff --git a/cmd/creinit/template/workflow/typescriptConfHTTP/main.ts.tpl b/cmd/creinit/template/workflow/typescriptConfHTTP/main.ts.tpl
index c12bc427..737c2b1e 100644
--- a/cmd/creinit/template/workflow/typescriptConfHTTP/main.ts.tpl
+++ b/cmd/creinit/template/workflow/typescriptConfHTTP/main.ts.tpl
@@ -56,7 +56,7 @@ const fetchResult = (sendRequester: ConfidentialHTTPSendRequester, config: Confi
return json(response) as ResponseValues
}
-const onCronTrigger = (runtime: Runtime) => {
+export const onCronTrigger = (runtime: Runtime) => {
runtime.log('Confidential HTTP workflow triggered.')
const confHTTPClient = new ConfidentialHTTPClient()
@@ -75,7 +75,7 @@ const onCronTrigger = (runtime: Runtime) => {
}
}
-const initWorkflow = (config: Config) => {
+export const initWorkflow = (config: Config) => {
const cron = new CronCapability()
return [handler(cron.trigger({ schedule: config.schedule }), onCronTrigger)]
diff --git a/cmd/creinit/template/workflow/typescriptConfHTTP/package.json.tpl b/cmd/creinit/template/workflow/typescriptConfHTTP/package.json.tpl
index 1cc95d48..e0af3745 100644
--- a/cmd/creinit/template/workflow/typescriptConfHTTP/package.json.tpl
+++ b/cmd/creinit/template/workflow/typescriptConfHTTP/package.json.tpl
@@ -8,7 +8,7 @@
},
"license": "UNLICENSED",
"dependencies": {
- "@chainlink/cre-sdk": "^1.0.9",
+ "@chainlink/cre-sdk": "^1.1.1",
"zod": "3.25.76"
},
"devDependencies": {
diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/main.test.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/main.test.ts.tpl
new file mode 100644
index 00000000..c44aa6e6
--- /dev/null
+++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/main.test.ts.tpl
@@ -0,0 +1,286 @@
+import { HTTPClient, consensusIdenticalAggregation, getNetwork, TxStatus } from "@chainlink/cre-sdk";
+import { describe, expect } from "bun:test";
+import {
+ newTestRuntime,
+ test,
+ HttpActionsMock,
+ EvmMock,
+} from "@chainlink/cre-sdk/test";
+import { initWorkflow, onCronTrigger, onLogTrigger, fetchReserveInfo } from "./main";
+import type { Config } from "./main";
+import { type Address, decodeFunctionData, encodeFunctionData, encodeFunctionResult } from "viem";
+import { BalanceReader, IERC20, MessageEmitter } from "../contracts/abi";
+
+const mockConfig: Config = {
+ schedule: "0 0 * * *",
+ url: "https://example.com/api/por",
+ evms: [
+ {
+ tokenAddress: "0x1234567890123456789012345678901234567890",
+ porAddress: "0x2234567890123456789012345678901234567890",
+ proxyAddress: "0x3234567890123456789012345678901234567890",
+ balanceReaderAddress: "0x4234567890123456789012345678901234567890",
+ messageEmitterAddress: "0x5234567890123456789012345678901234567890",
+ chainSelectorName: "ethereum-testnet-sepolia",
+ gasLimit: "1000000",
+ },
+ ],
+};
+
+/**
+ * Helper to set up all EVM mocks for the PoR workflow.
+ * Mocks three contract call paths:
+ * 1. BalanceReader.getNativeBalances - returns mock native token balances
+ * 2. IERC20.totalSupply - returns mock total supply
+ * 3. MessageEmitter.getLastMessage - returns mock message (for log trigger)
+ * 4. WriteReport - returns success for reserve updates
+ */
+const setupEVMMocks = (config: Config) => {
+ const network = getNetwork({
+ chainFamily: "evm",
+ chainSelectorName: config.evms[0].chainSelectorName,
+ isTestnet: true,
+ });
+
+ if (!network) {
+ throw new Error(`Network not found for chain selector: ${config.evms[0].chainSelectorName}`);
+ }
+
+ const evmMock = EvmMock.testInstance(network.chainSelector.selector);
+
+ // Mock contract calls - route based on target address and function signature
+ evmMock.callContract = (req) => {
+ const toAddress = Buffer.from(req.call?.to || new Uint8Array()).toString("hex").toLowerCase();
+ const callData = Buffer.from(req.call?.data || new Uint8Array());
+
+ // BalanceReader.getNativeBalances
+ if (toAddress === config.evms[0].balanceReaderAddress.slice(2).toLowerCase()) {
+ const decoded = decodeFunctionData({
+ abi: BalanceReader,
+ data: `0x${callData.toString("hex")}` as Address,
+ });
+
+ if (decoded.functionName === "getNativeBalances") {
+ const addresses = decoded.args[0] as Address[];
+ expect(addresses.length).toBeGreaterThan(0);
+
+ // Return mock balance for each address (0.5 ETH in wei)
+ const mockBalances = addresses.map(() => 500000000000000000n);
+ const resultData = encodeFunctionResult({
+ abi: BalanceReader,
+ functionName: "getNativeBalances",
+ result: mockBalances,
+ });
+
+ return {
+ data: Buffer.from(resultData.slice(2), "hex"),
+ };
+ }
+ }
+
+ // IERC20.totalSupply
+ if (toAddress === config.evms[0].tokenAddress.slice(2).toLowerCase()) {
+ const decoded = decodeFunctionData({
+ abi: IERC20,
+ data: `0x${callData.toString("hex")}` as Address,
+ });
+
+ if (decoded.functionName === "totalSupply") {
+ // Return mock total supply (1 token with 18 decimals)
+ const mockSupply = 1000000000000000000n;
+ const resultData = encodeFunctionResult({
+ abi: IERC20,
+ functionName: "totalSupply",
+ result: mockSupply,
+ });
+
+ return {
+ data: Buffer.from(resultData.slice(2), "hex"),
+ };
+ }
+ }
+
+ // MessageEmitter.getLastMessage
+ if (toAddress === config.evms[0].messageEmitterAddress.slice(2).toLowerCase()) {
+ const decoded = decodeFunctionData({
+ abi: MessageEmitter,
+ data: `0x${callData.toString("hex")}` as Address,
+ });
+
+ if (decoded.functionName === "getLastMessage") {
+ // Verify the emitter address parameter is passed correctly
+ const emitterArg = decoded.args[0] as string;
+ expect(emitterArg).toBeDefined();
+
+ const mockMessage = "Test message from contract";
+ const resultData = encodeFunctionResult({
+ abi: MessageEmitter,
+ functionName: "getLastMessage",
+ result: mockMessage,
+ });
+
+ return {
+ data: Buffer.from(resultData.slice(2), "hex"),
+ };
+ }
+ }
+
+ throw new Error(`Unmocked contract call to ${toAddress} with data ${callData.toString("hex")}`);
+ };
+
+ // Mock writeReport for updateReserves
+ evmMock.writeReport = (req) => {
+ // Convert Uint8Array receiver to hex string for comparison
+ const receiverHex = `0x${Buffer.from(req.receiver || new Uint8Array()).toString("hex")}`;
+ expect(receiverHex.toLowerCase()).toBe(config.evms[0].proxyAddress.toLowerCase());
+ expect(req.report).toBeDefined();
+ // gasLimit is bigint, config has string - compare the values
+ expect(req.gasConfig?.gasLimit?.toString()).toBe(config.evms[0].gasLimit);
+
+ return {
+ txStatus: TxStatus.SUCCESS,
+ txHash: new Uint8Array(Buffer.from("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "hex")),
+ errorMessage: "",
+ };
+ };
+
+ return evmMock;
+};
+
+describe("fetchReserveInfo", () => {
+ test("fetches and parses reserve info using HTTP capability", async () => {
+ const runtime = newTestRuntime();
+ runtime.config = mockConfig;
+
+ const httpMock = HttpActionsMock.testInstance();
+
+ const mockPORResponse = {
+ accountName: "test-account",
+ totalTrust: 1500000,
+ totalToken: 1500000,
+ ripcord: false,
+ updatedAt: "2024-01-15T12:00:00Z",
+ };
+
+ httpMock.sendRequest = (req) => {
+ expect(req.method).toBe("GET");
+ expect(req.url).toBe(mockConfig.url);
+ return {
+ statusCode: 200,
+ body: new TextEncoder().encode(JSON.stringify(mockPORResponse)),
+ headers: {},
+ };
+ };
+
+ const httpClient = new HTTPClient();
+ const result = httpClient
+ .sendRequest(runtime, fetchReserveInfo, consensusIdenticalAggregation())(mockConfig)
+ .result();
+
+ expect(result.totalReserve).toBe(mockPORResponse.totalToken);
+ expect(result.lastUpdated).toBeInstanceOf(Date);
+ });
+});
+
+describe("onCronTrigger", () => {
+ test("executes full PoR workflow with all EVM calls", () => {
+ const runtime = newTestRuntime();
+ runtime.config = mockConfig;
+
+ // Setup HTTP mock for reserve info
+ const httpMock = HttpActionsMock.testInstance();
+ const mockPORResponse = {
+ accountName: "TrueUSD",
+ totalTrust: 1000000,
+ totalToken: 1000000,
+ ripcord: false,
+ updatedAt: "2023-01-01T00:00:00Z",
+ };
+
+ httpMock.sendRequest = (req) => {
+ expect(req.method).toBe("GET");
+ expect(req.url).toBe(mockConfig.url);
+ return {
+ statusCode: 200,
+ body: new TextEncoder().encode(JSON.stringify(mockPORResponse)),
+ headers: {},
+ };
+ };
+
+ // Setup all EVM mocks
+ setupEVMMocks(mockConfig);
+
+ // Execute trigger with mock payload
+ const result = onCronTrigger(runtime, {
+ scheduledExecutionTime: {
+ seconds: 1752514917n,
+ nanos: 0,
+ },
+ });
+
+ // Result should be the totalToken from mock response
+ expect(result).toBeDefined();
+ expect(typeof result).toBe("string");
+
+ // Verify expected log messages were produced
+ const logs = runtime.getLogs().map((log) => Buffer.from(log).toString("utf-8"));
+ expect(logs.some((log) => log.includes("fetching por"))).toBe(true);
+ expect(logs.some((log) => log.includes("ReserveInfo"))).toBe(true);
+ expect(logs.some((log) => log.includes("TotalSupply"))).toBe(true);
+ expect(logs.some((log) => log.includes("TotalReserveScaled"))).toBe(true);
+ expect(logs.some((log) => log.includes("NativeTokenBalance"))).toBe(true);
+ });
+
+ test("validates scheduledExecutionTime is present", () => {
+ const runtime = newTestRuntime();
+ runtime.config = mockConfig;
+
+ expect(() => onCronTrigger(runtime, {})).toThrow("Scheduled execution time is required");
+ });
+});
+
+describe("onLogTrigger", () => {
+ test("retrieves and returns message from contract", () => {
+ const runtime = newTestRuntime();
+ runtime.config = mockConfig;
+
+ // Setup EVM mock for MessageEmitter
+ setupEVMMocks(mockConfig);
+
+ // Create mock EVMLog payload matching the expected structure
+ // topics[1] should contain the emitter address (padded to 32 bytes)
+ const mockLog = {
+ topics: [
+ Buffer.from("1234567890123456789012345678901234567890123456789012345678901234", "hex"),
+ Buffer.from("000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd", "hex"),
+ Buffer.from("000000000000000000000000000000000000000000000000000000006716eb80", "hex"),
+ ],
+ data: Buffer.from("", "hex"),
+ blockNumber: { value: 100n },
+ };
+
+ const result = onLogTrigger(runtime, mockLog);
+
+ expect(result).toBe("Test message from contract");
+
+ // Verify log message
+ const logs = runtime.getLogs().map((log) => Buffer.from(log).toString("utf-8"));
+ expect(logs.some((log) => log.includes("Message retrieved from the contract"))).toBe(true);
+ });
+});
+
+describe("initWorkflow", () => {
+ test("returns two handlers with correct configuration", () => {
+ const testSchedule = "*/10 * * * *";
+ const config = { ...mockConfig, schedule: testSchedule };
+
+ const handlers = initWorkflow(config);
+
+ expect(handlers).toBeArray();
+ expect(handlers).toHaveLength(2);
+ expect(handlers[0].trigger.config.schedule).toBe(testSchedule);
+ expect(handlers[0].fn.name).toBe("onCronTrigger");
+ expect(handlers[1].trigger.config).toHaveProperty("addresses");
+ expect(handlers[1].fn.name).toBe("onLogTrigger");
+ });
+});
diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/main.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/main.ts.tpl
index 85271301..9f14c7f2 100644
--- a/cmd/creinit/template/workflow/typescriptPorExampleDev/main.ts.tpl
+++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/main.ts.tpl
@@ -37,7 +37,7 @@ const configSchema = z.object({
),
})
-type Config = z.infer
+export type Config = z.infer
interface PORResponse {
accountName: string
@@ -56,7 +56,7 @@ interface ReserveInfo {
const safeJsonStringify = (obj: any): string =>
JSON.stringify(obj, (_, value) => (typeof value === 'bigint' ? value.toString() : value), 2)
-const fetchReserveInfo = (sendRequester: HTTPSendRequester, config: Config): ReserveInfo => {
+export const fetchReserveInfo = (sendRequester: HTTPSendRequester, config: Config): ReserveInfo => {
const response = sendRequester.sendRequest({ method: 'GET', url: config.url }).result()
if (response.statusCode !== 200) {
@@ -319,7 +319,7 @@ const getLastMessage = (
return message
}
-const onCronTrigger = (runtime: Runtime, payload: CronPayload): string => {
+export const onCronTrigger = (runtime: Runtime, payload: CronPayload): string => {
if (!payload.scheduledExecutionTime) {
throw new Error('Scheduled execution time is required')
}
@@ -329,7 +329,7 @@ const onCronTrigger = (runtime: Runtime, payload: CronPayload): string =
return doPOR(runtime)
}
-const onLogTrigger = (runtime: Runtime, payload: EVMLog): string => {
+export const onLogTrigger = (runtime: Runtime, payload: EVMLog): string => {
runtime.log('Running LogTrigger')
const topics = payload.topics
@@ -350,7 +350,7 @@ const onLogTrigger = (runtime: Runtime, payload: EVMLog): string => {
return message
}
-const initWorkflow = (config: Config) => {
+export const initWorkflow = (config: Config) => {
const cronTrigger = new CronCapability()
const network = getNetwork({
chainFamily: 'evm',
diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/package.json.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/package.json.tpl
index 38cc533b..e613002f 100644
--- a/cmd/creinit/template/workflow/typescriptPorExampleDev/package.json.tpl
+++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/package.json.tpl
@@ -8,7 +8,7 @@
},
"license": "UNLICENSED",
"dependencies": {
- "@chainlink/cre-sdk": "^1.0.9",
+ "@chainlink/cre-sdk": "^1.1.1",
"viem": "2.34.0",
"zod": "3.25.76"
},
diff --git a/cmd/creinit/template/workflow/typescriptSimpleExample/main.test.ts.tpl b/cmd/creinit/template/workflow/typescriptSimpleExample/main.test.ts.tpl
new file mode 100644
index 00000000..d108aadb
--- /dev/null
+++ b/cmd/creinit/template/workflow/typescriptSimpleExample/main.test.ts.tpl
@@ -0,0 +1,42 @@
+import { describe, expect } from "bun:test";
+import { newTestRuntime, test } from "@chainlink/cre-sdk/test";
+import { onCronTrigger, initWorkflow } from "./main";
+import type { Config } from "./main";
+
+describe("onCronTrigger", () => {
+ test("logs message and returns greeting", async () => {
+ const config: Config = { schedule: "*/5 * * * *" };
+ const runtime = newTestRuntime();
+ runtime.config = config;
+
+ const result = onCronTrigger(runtime);
+
+ expect(result).toBe("Hello world!");
+ const logs = runtime.getLogs();
+ expect(logs).toContain("Hello world! Workflow triggered.");
+ });
+});
+
+describe("initWorkflow", () => {
+ test("returns one handler with correct cron schedule", async () => {
+ const testSchedule = "0 0 * * *";
+ const config: Config = { schedule: testSchedule };
+
+ const handlers = initWorkflow(config);
+
+ expect(handlers).toBeArray();
+ expect(handlers).toHaveLength(1);
+ expect(handlers[0].trigger.config.schedule).toBe(testSchedule);
+ });
+
+ test("handler executes onCronTrigger and returns result", async () => {
+ const config: Config = { schedule: "*/5 * * * *" };
+ const runtime = newTestRuntime();
+ runtime.config = config;
+ const handlers = initWorkflow(config);
+
+ const result = handlers[0].fn(runtime, {});
+
+ expect(result).toBe(onCronTrigger(runtime));
+ });
+});
diff --git a/cmd/creinit/template/workflow/typescriptSimpleExample/main.ts.tpl b/cmd/creinit/template/workflow/typescriptSimpleExample/main.ts.tpl
index aada0405..36682c22 100644
--- a/cmd/creinit/template/workflow/typescriptSimpleExample/main.ts.tpl
+++ b/cmd/creinit/template/workflow/typescriptSimpleExample/main.ts.tpl
@@ -1,15 +1,15 @@
import { CronCapability, handler, Runner, type Runtime } from "@chainlink/cre-sdk";
-type Config = {
+export type Config = {
schedule: string;
};
-const onCronTrigger = (runtime: Runtime): string => {
+export const onCronTrigger = (runtime: Runtime): string => {
runtime.log("Hello world! Workflow triggered.");
return "Hello world!";
};
-const initWorkflow = (config: Config) => {
+export const initWorkflow = (config: Config) => {
const cron = new CronCapability();
return [
diff --git a/cmd/creinit/template/workflow/typescriptSimpleExample/package.json.tpl b/cmd/creinit/template/workflow/typescriptSimpleExample/package.json.tpl
index cddfabf3..0dc43de0 100644
--- a/cmd/creinit/template/workflow/typescriptSimpleExample/package.json.tpl
+++ b/cmd/creinit/template/workflow/typescriptSimpleExample/package.json.tpl
@@ -8,7 +8,7 @@
},
"license": "UNLICENSED",
"dependencies": {
- "@chainlink/cre-sdk": "^1.0.9"
+ "@chainlink/cre-sdk": "^1.1.1"
},
"devDependencies": {
"@types/bun": "1.2.21"
diff --git a/test/init_convert_simulate_go_test.go b/test/init_convert_simulate_go_test.go
index f1a1b33f..73c9fcbb 100644
--- a/test/init_convert_simulate_go_test.go
+++ b/test/init_convert_simulate_go_test.go
@@ -62,7 +62,7 @@ func TestE2EInit_ConvertToCustomBuild_Go(t *testing.T) {
require.DirExists(t, filepath.Join(workflowDirectory, "wasm"))
// Now make test-specific changes: FlagProof, constA/constB, Makefile FLAG
- mainPath := filepath.Join(workflowDirectory, "main.go")
+ mainPath := filepath.Join(workflowDirectory, "workflow.go")
mainBytes, err := os.ReadFile(mainPath)
require.NoError(t, err)
mainStr := string(mainBytes)