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)