it works
This commit is contained in:
parent
363efa1cca
commit
f25524791b
4 changed files with 370 additions and 86 deletions
217
README.md
217
README.md
|
|
@ -21,110 +21,193 @@ zig build
|
|||
# Release build (arm64)
|
||||
zig build -Doptimize=ReleaseFast
|
||||
|
||||
# Create Lambda deployment package
|
||||
zig build -Doptimize=ReleaseFast package
|
||||
|
||||
# Build for native target (e.g., for local testing)
|
||||
zig build -Dtarget=native
|
||||
|
||||
# Run tests
|
||||
zig build test -Dtarget=native
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- [lambda-zig](../../lambda-zig) - AWS Lambda runtime for Zig
|
||||
- [controlr](../../controlr) - Rinnai API client (provides `rinnai` module)
|
||||
- [lambda-zig](https://git.lerch.org/lobo/lambda-zig) - AWS Lambda runtime for Zig
|
||||
- [controlr](https://git.lerch.org/lobo/controlr) - Rinnai API client (provides `rinnai` module)
|
||||
|
||||
## Deployment Setup
|
||||
|
||||
Before deploying, you need to configure both AWS credentials and ASK CLI authentication.
|
||||
|
||||
### 1. AWS Credentials
|
||||
|
||||
AWS credentials are required for Lambda deployment. You can configure them in several ways:
|
||||
|
||||
#### Option A: AWS CLI Configuration (Recommended)
|
||||
|
||||
```bash
|
||||
# Configure default profile
|
||||
aws configure
|
||||
|
||||
# Or configure a named profile
|
||||
aws configure --profile personal
|
||||
```
|
||||
|
||||
This creates `~/.aws/credentials` and `~/.aws/config` files.
|
||||
|
||||
#### Option B: Environment Variables
|
||||
|
||||
```bash
|
||||
export AWS_ACCESS_KEY_ID=AKIA...
|
||||
export AWS_SECRET_ACCESS_KEY=...
|
||||
export AWS_DEFAULT_REGION=us-west-2
|
||||
```
|
||||
|
||||
#### Using a Non-Default AWS Profile
|
||||
|
||||
If your default AWS profile is your work account but you want to deploy to your personal account:
|
||||
|
||||
```bash
|
||||
# Set the profile for the current shell session
|
||||
export AWS_PROFILE=personal
|
||||
|
||||
# Or specify it per-command
|
||||
zig build awslambda_deploy -Dprofile=personal
|
||||
zig build deploy -Dprofile=personal
|
||||
```
|
||||
|
||||
You can also set the region:
|
||||
|
||||
```bash
|
||||
zig build awslambda_deploy -Dprofile=personal -Dregion=us-west-2
|
||||
```
|
||||
|
||||
### 2. ASK CLI Authentication
|
||||
|
||||
The ASK CLI requires authentication with your Amazon Developer account to deploy Alexa skills.
|
||||
|
||||
#### First-Time Setup
|
||||
|
||||
```bash
|
||||
# Install dependencies (if not already installed)
|
||||
bun install
|
||||
|
||||
# Configure ASK CLI (opens browser for Amazon login). Note this takes an ungodly
|
||||
# amount of time to do anything and it will look like everything is hung
|
||||
bun x ask configure
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Open your browser to sign in with your Amazon Developer account
|
||||
2. Store credentials in `~/.ask/cli_config`
|
||||
|
||||
#### Verify Authentication
|
||||
|
||||
```bash
|
||||
bun x ask smapi list-skills-for-vendor
|
||||
```
|
||||
|
||||
### 3. Rinnai Credentials
|
||||
|
||||
The Lambda function needs your Rinnai account credentials to authenticate with the water heater API.
|
||||
|
||||
Create a `.env` file in the project root (this file is gitignored):
|
||||
|
||||
```bash
|
||||
# .env
|
||||
COGNITO_USERNAME=your@email.com
|
||||
COGNITO_PASSWORD=your_password
|
||||
```
|
||||
|
||||
These credentials will be automatically deployed to Lambda when you use the `-Denv-file=.env` option.
|
||||
|
||||
## Build Steps
|
||||
|
||||
| Step | Description |
|
||||
|------|-------------|
|
||||
| `zig build` | Build the bootstrap executable |
|
||||
| `zig build test` | Run unit tests |
|
||||
| `zig build awslambda_package` | Package Lambda function into zip |
|
||||
| `zig build awslambda_iam` | Create/verify IAM role |
|
||||
| `zig build awslambda_deploy` | Deploy Lambda function to AWS |
|
||||
| `zig build awslambda_run` | Invoke the deployed Lambda function |
|
||||
| `zig build ask_deploy` | Deploy Alexa skill metadata |
|
||||
| `zig build deploy` | Deploy both Lambda and Alexa skill |
|
||||
|
||||
### Build Options
|
||||
|
||||
| Option | Description | Default |
|
||||
|--------|-------------|---------|
|
||||
| `-Doptimize=ReleaseFast` | Build with optimizations | Debug |
|
||||
| `-Dtarget=native` | Build for local machine | aarch64-linux |
|
||||
| `-Dfunction-name=NAME` | Lambda function name | zig-fn |
|
||||
| `-Dprofile=PROFILE` | AWS profile to use | default |
|
||||
| `-Dregion=REGION` | AWS region | from profile |
|
||||
| `-Drole-name=ROLE` | IAM role name | lambda_basic_execution |
|
||||
| `-Dpayload=JSON` | Payload for `awslambda_run` | {} |
|
||||
| `-Denv-file=PATH` | Environment variables file | none |
|
||||
| `-Dallow-principal=PRINCIPAL` | AWS service principal to grant invoke permission | none |
|
||||
|
||||
## Deployment
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- AWS CLI configured with appropriate credentials
|
||||
- mise (for zig and bun version management)
|
||||
Before deploying, ensure you have:
|
||||
|
||||
### 1. Build the Package
|
||||
1. **AWS Account** with credentials configured (see [Deployment Setup](#deployment-setup))
|
||||
2. **Amazon Developer Account** with ASK CLI authenticated (`bun x ask configure`)
|
||||
3. **Rinnai credentials** in `.env` file (see [Rinnai Credentials](#3-rinnai-credentials))
|
||||
|
||||
### Full Deployment (Lambda + Alexa Skill)
|
||||
|
||||
```bash
|
||||
mise exec -- zig build -Doptimize=ReleaseFast package
|
||||
zig build deploy -Doptimize=ReleaseFast \
|
||||
-Dfunction-name=water-recirculation \
|
||||
-Dprofile=personal \
|
||||
-Dregion=us-west-2 \
|
||||
-Denv-file=.env \
|
||||
-Dallow-principal=alexa-appkit.amazon.com
|
||||
```
|
||||
|
||||
This creates `function.zip` containing the arm64 bootstrap executable.
|
||||
This command will:
|
||||
1. Build the Lambda function for arm64
|
||||
2. Package it into a zip file
|
||||
3. Create/update the Lambda function in AWS
|
||||
4. Set environment variables from `.env`
|
||||
5. Grant Alexa Skills Kit permission to invoke the function
|
||||
6. Deploy the Alexa skill metadata via ASK CLI
|
||||
|
||||
### 2. Create Lambda Function (first time only)
|
||||
### Lambda Only
|
||||
|
||||
```bash
|
||||
aws lambda create-function \
|
||||
--function-name water-recirculation \
|
||||
--runtime provided.al2023 \
|
||||
--handler bootstrap \
|
||||
--architectures arm64 \
|
||||
--role arn:aws:iam::ACCOUNT_ID:role/lambda_basic_execution \
|
||||
--zip-file fileb://function.zip \
|
||||
--timeout 30 \
|
||||
--memory-size 128
|
||||
zig build awslambda_deploy -Doptimize=ReleaseFast \
|
||||
-Dfunction-name=water-recirculation \
|
||||
-Dprofile=personal \
|
||||
-Dregion=us-west-2 \
|
||||
-Denv-file=.env \
|
||||
-Dallow-principal=alexa-appkit.amazon.com
|
||||
```
|
||||
|
||||
### 3. Set Environment Variables
|
||||
### Alexa Skill Only
|
||||
|
||||
```bash
|
||||
aws lambda update-function-configuration \
|
||||
--function-name water-recirculation \
|
||||
--environment "Variables={COGNITO_USERNAME=your@email.com,COGNITO_PASSWORD=your_password}"
|
||||
```
|
||||
|
||||
### 4. Update Function Code (subsequent deploys)
|
||||
|
||||
```bash
|
||||
mise exec -- zig build -Doptimize=ReleaseFast package
|
||||
|
||||
aws lambda update-function-code \
|
||||
--function-name water-recirculation \
|
||||
--zip-file fileb://function.zip
|
||||
```
|
||||
|
||||
### 5. Deploy Alexa Skill
|
||||
|
||||
First time setup - configure ASK CLI (opens browser for Amazon login):
|
||||
|
||||
```bash
|
||||
mise exec -- bunx ask-cli configure
|
||||
```
|
||||
|
||||
Deploy the skill:
|
||||
|
||||
```bash
|
||||
mise exec -- bunx ask-cli deploy
|
||||
```
|
||||
|
||||
This will:
|
||||
- Create the Alexa skill in your developer account
|
||||
- Upload the interaction model
|
||||
- Link to the Lambda endpoint
|
||||
|
||||
After deployment, add the Alexa Skills Kit trigger permission to Lambda:
|
||||
|
||||
```bash
|
||||
aws lambda add-permission \
|
||||
--function-name water-recirculation \
|
||||
--statement-id alexa-skill \
|
||||
--action lambda:InvokeFunction \
|
||||
--principal alexa-appkit.amazon.com \
|
||||
--event-source-token amzn1.ask.skill.YOUR_SKILL_ID
|
||||
zig build ask_deploy
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
water_recirculation/
|
||||
├── build.zig # Build configuration (defaults to arm64-linux)
|
||||
├── build.zig # Build configuration
|
||||
├── build.zig.zon # Dependencies (lambda-zig, controlr)
|
||||
├── .env # Rinnai credentials (gitignored, create locally)
|
||||
├── ask-resources.json # ASK CLI deployment config
|
||||
├── package.json # Node.js deps for ASK CLI
|
||||
├── src/
|
||||
│ └── main.zig # Alexa request handler
|
||||
│ └── main.zig # Alexa request handler + tests
|
||||
├── skill-package/
|
||||
│ ├── skill.json # Alexa skill manifest
|
||||
│ └── interactionModels/
|
||||
│ └── custom/
|
||||
│ └── en-US.json # Interaction model
|
||||
└── function.zip # Lambda deployment package (after build)
|
||||
```
|
||||
|
||||
## Sample Utterances
|
||||
|
|
|
|||
27
build.zig
27
build.zig
|
|
@ -1,4 +1,5 @@
|
|||
const std = @import("std");
|
||||
const lambda_zig = @import("lambda_zig");
|
||||
|
||||
pub fn build(b: *std.Build) !void {
|
||||
// Default to aarch64-linux for Lambda Graviton deployment
|
||||
|
|
@ -42,18 +43,24 @@ pub fn build(b: *std.Build) !void {
|
|||
|
||||
b.installArtifact(exe);
|
||||
|
||||
// Create a step to package for Lambda
|
||||
const package_step = b.step("package", "Package for AWS Lambda (arm64) deployment");
|
||||
// Configure Lambda build steps (awslambda_package, awslambda_deploy, etc.)
|
||||
try lambda_zig.configureBuild(b, lambda_zig_dep, exe);
|
||||
|
||||
// After installing, create a zip
|
||||
const install_step = b.getInstallStep();
|
||||
|
||||
// Add a system command to create zip (requires zip to be installed)
|
||||
const zip_cmd = b.addSystemCommand(&.{
|
||||
"zip", "-j", "function.zip", "zig-out/bin/bootstrap",
|
||||
// ASK CLI deploy step for Alexa skill metadata
|
||||
const ask_deploy_cmd = b.addSystemCommand(&.{
|
||||
"bun", "x", "ask", "deploy", "--target", "skill-metadata",
|
||||
});
|
||||
zip_cmd.step.dependOn(install_step);
|
||||
package_step.dependOn(&zip_cmd.step);
|
||||
const ask_deploy_step = b.step("ask_deploy", "Deploy Alexa skill metadata via ASK CLI");
|
||||
ask_deploy_step.dependOn(&ask_deploy_cmd.step);
|
||||
|
||||
// Full deploy step - deploys both Lambda function and Alexa skill
|
||||
const full_deploy_step = b.step("deploy", "Deploy Lambda function and Alexa skill");
|
||||
// Lambda deploy (awslambda_deploy) is added by lambda_zig.configureBuild
|
||||
// We need to get a reference to it - it's registered as "awslambda_deploy"
|
||||
if (b.top_level_steps.get("awslambda_deploy")) |lambda_deploy| {
|
||||
full_deploy_step.dependOn(&lambda_deploy.step);
|
||||
}
|
||||
full_deploy_step.dependOn(&ask_deploy_cmd.step);
|
||||
|
||||
// Test step - reuses the same target query, tests run via emulation or on native arm64
|
||||
const test_module = b.createModule(.{
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@
|
|||
.hash = "controlr-0.1.0-upFm0LtgAAAo85RiRDa0WmSrORIhAa5bCF8UdT9rDyUk",
|
||||
},
|
||||
.lambda_zig = .{
|
||||
.url = "git+https://git.lerch.org/lobo/lambda-zig#183d2d912c41ca721c8d18e5c258e4472d38db70",
|
||||
.hash = "lambda_zig-0.1.0-_G43_6YQAQD-ahqtf3DQpJroP__spvt4U_uI5TtMZ4Xv",
|
||||
.url = "git+https://git.lerch.org/lobo/lambda-zig#b420abb0a145e8c9bb151606c124ed380cb744e9",
|
||||
.hash = "lambda_zig-0.1.0-_G43_zRAAQA7RdBlHSxhmfSRrlvOOk3DJhfDJfimhneA",
|
||||
},
|
||||
},
|
||||
.paths = .{
|
||||
|
|
|
|||
208
src/main.zig
208
src/main.zig
|
|
@ -2,6 +2,7 @@ const std = @import("std");
|
|||
const json = std.json;
|
||||
const lambda = @import("lambda_runtime");
|
||||
const rinnai = @import("rinnai");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const log = std.log.scoped(.alexa);
|
||||
|
||||
|
|
@ -19,24 +20,24 @@ fn handler(allocator: std.mem.Allocator, event_data: []const u8) anyerror![]cons
|
|||
|
||||
// Parse the Alexa request
|
||||
const parsed = json.parseFromSlice(json.Value, allocator, event_data, .{}) catch |err| {
|
||||
log.err("Failed to parse Alexa request: {}", .{err});
|
||||
if (!builtin.is_test) log.err("Failed to parse Alexa request: {}", .{err});
|
||||
return buildAlexaResponse(allocator, "I couldn't understand that request.", true);
|
||||
};
|
||||
defer parsed.deinit();
|
||||
|
||||
// Get request type
|
||||
const request_obj = parsed.value.object.get("request") orelse {
|
||||
log.err("No 'request' field in Alexa event", .{});
|
||||
if (!builtin.is_test) log.err("No 'request' field in Alexa event", .{});
|
||||
return buildAlexaResponse(allocator, "Invalid request format.", true);
|
||||
};
|
||||
|
||||
const request_type = request_obj.object.get("type") orelse {
|
||||
log.err("No 'type' field in request", .{});
|
||||
if (!builtin.is_test) log.err("No 'type' field in request", .{});
|
||||
return buildAlexaResponse(allocator, "Invalid request format.", true);
|
||||
};
|
||||
|
||||
const request_type_str = if (request_type == .string) request_type.string else {
|
||||
log.err("Request type is not a string", .{});
|
||||
if (!builtin.is_test) log.err("Request type is not a string", .{});
|
||||
return buildAlexaResponse(allocator, "Invalid request format.", true);
|
||||
};
|
||||
|
||||
|
|
@ -57,17 +58,17 @@ fn handler(allocator: std.mem.Allocator, event_data: []const u8) anyerror![]cons
|
|||
/// Handle Alexa intent requests
|
||||
fn handleIntentRequest(allocator: std.mem.Allocator, request_obj: json.Value) ![]const u8 {
|
||||
const intent_obj = request_obj.object.get("intent") orelse {
|
||||
log.err("No 'intent' field in IntentRequest", .{});
|
||||
if (!builtin.is_test) log.err("No 'intent' field in IntentRequest", .{});
|
||||
return buildAlexaResponse(allocator, "I couldn't understand your intent.", true);
|
||||
};
|
||||
|
||||
const intent_name_val = intent_obj.object.get("name") orelse {
|
||||
log.err("No 'name' field in intent", .{});
|
||||
if (!builtin.is_test) log.err("No 'name' field in intent", .{});
|
||||
return buildAlexaResponse(allocator, "I couldn't understand your intent.", true);
|
||||
};
|
||||
|
||||
const intent_name = if (intent_name_val == .string) intent_name_val.string else {
|
||||
log.err("Intent name is not a string", .{});
|
||||
if (!builtin.is_test) log.err("Intent name is not a string", .{});
|
||||
return buildAlexaResponse(allocator, "I couldn't understand your intent.", true);
|
||||
};
|
||||
|
||||
|
|
@ -163,3 +164,196 @@ fn buildAlexaResponse(allocator: std.mem.Allocator, speech: []const u8, end_sess
|
|||
\\{{"version":"1.0","response":{{"outputSpeech":{{"type":"PlainText","text":"{s}"}},"shouldEndSession":{s}}}}}
|
||||
, .{ escaped_speech.items, end_session_str });
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Tests
|
||||
// =============================================================================
|
||||
|
||||
test "buildAlexaResponse with speech and end session" {
|
||||
const allocator = std.testing.allocator;
|
||||
const response = try buildAlexaResponse(allocator, "Hello world", true);
|
||||
defer allocator.free(response);
|
||||
|
||||
const parsed = try json.parseFromSlice(json.Value, allocator, response, .{});
|
||||
defer parsed.deinit();
|
||||
|
||||
try std.testing.expectEqualStrings("1.0", parsed.value.object.get("version").?.string);
|
||||
const resp = parsed.value.object.get("response").?.object;
|
||||
try std.testing.expect(resp.get("shouldEndSession").?.bool == true);
|
||||
try std.testing.expectEqualStrings("PlainText", resp.get("outputSpeech").?.object.get("type").?.string);
|
||||
try std.testing.expectEqualStrings("Hello world", resp.get("outputSpeech").?.object.get("text").?.string);
|
||||
}
|
||||
|
||||
test "buildAlexaResponse with speech and keep session open" {
|
||||
const allocator = std.testing.allocator;
|
||||
const response = try buildAlexaResponse(allocator, "What can I help with?", false);
|
||||
defer allocator.free(response);
|
||||
|
||||
const parsed = try json.parseFromSlice(json.Value, allocator, response, .{});
|
||||
defer parsed.deinit();
|
||||
|
||||
const resp = parsed.value.object.get("response").?.object;
|
||||
try std.testing.expect(resp.get("shouldEndSession").?.bool == false);
|
||||
}
|
||||
|
||||
test "buildAlexaResponse empty speech (SessionEndedRequest)" {
|
||||
const allocator = std.testing.allocator;
|
||||
const response = try buildAlexaResponse(allocator, "", true);
|
||||
defer allocator.free(response);
|
||||
|
||||
const parsed = try json.parseFromSlice(json.Value, allocator, response, .{});
|
||||
defer parsed.deinit();
|
||||
|
||||
const resp = parsed.value.object.get("response").?.object;
|
||||
try std.testing.expect(resp.get("shouldEndSession").?.bool == true);
|
||||
try std.testing.expect(resp.get("outputSpeech") == null);
|
||||
}
|
||||
|
||||
test "buildAlexaResponse escapes special characters" {
|
||||
const allocator = std.testing.allocator;
|
||||
const response = try buildAlexaResponse(allocator, "Say \"hello\"\nNew line", true);
|
||||
defer allocator.free(response);
|
||||
|
||||
// Should parse as valid JSON (escaping worked)
|
||||
const parsed = try json.parseFromSlice(json.Value, allocator, response, .{});
|
||||
defer parsed.deinit();
|
||||
|
||||
const text = parsed.value.object.get("response").?.object.get("outputSpeech").?.object.get("text").?.string;
|
||||
try std.testing.expectEqualStrings("Say \"hello\"\nNew line", text);
|
||||
}
|
||||
|
||||
test "handler returns error response for invalid JSON" {
|
||||
const allocator = std.testing.allocator;
|
||||
const response = try handler(allocator, "not valid json");
|
||||
defer allocator.free(response);
|
||||
|
||||
const parsed = try json.parseFromSlice(json.Value, allocator, response, .{});
|
||||
defer parsed.deinit();
|
||||
|
||||
const text = parsed.value.object.get("response").?.object.get("outputSpeech").?.object.get("text").?.string;
|
||||
try std.testing.expectEqualStrings("I couldn't understand that request.", text);
|
||||
}
|
||||
|
||||
test "handler returns error for missing request field" {
|
||||
const allocator = std.testing.allocator;
|
||||
const response = try handler(allocator, "{}");
|
||||
defer allocator.free(response);
|
||||
|
||||
const parsed = try json.parseFromSlice(json.Value, allocator, response, .{});
|
||||
defer parsed.deinit();
|
||||
|
||||
const text = parsed.value.object.get("response").?.object.get("outputSpeech").?.object.get("text").?.string;
|
||||
try std.testing.expectEqualStrings("Invalid request format.", text);
|
||||
}
|
||||
|
||||
test "handler handles LaunchRequest" {
|
||||
const allocator = std.testing.allocator;
|
||||
const launch_request =
|
||||
\\{"request":{"type":"LaunchRequest"}}
|
||||
;
|
||||
const response = try handler(allocator, launch_request);
|
||||
defer allocator.free(response);
|
||||
|
||||
const parsed = try json.parseFromSlice(json.Value, allocator, response, .{});
|
||||
defer parsed.deinit();
|
||||
|
||||
const resp = parsed.value.object.get("response").?.object;
|
||||
try std.testing.expect(resp.get("shouldEndSession").?.bool == false);
|
||||
const text = resp.get("outputSpeech").?.object.get("text").?.string;
|
||||
try std.testing.expect(std.mem.indexOf(u8, text, "hot water") != null);
|
||||
}
|
||||
|
||||
test "handler handles SessionEndedRequest" {
|
||||
const allocator = std.testing.allocator;
|
||||
const session_ended =
|
||||
\\{"request":{"type":"SessionEndedRequest"}}
|
||||
;
|
||||
const response = try handler(allocator, session_ended);
|
||||
defer allocator.free(response);
|
||||
|
||||
const parsed = try json.parseFromSlice(json.Value, allocator, response, .{});
|
||||
defer parsed.deinit();
|
||||
|
||||
const resp = parsed.value.object.get("response").?.object;
|
||||
try std.testing.expect(resp.get("shouldEndSession").?.bool == true);
|
||||
try std.testing.expect(resp.get("outputSpeech") == null);
|
||||
}
|
||||
|
||||
test "handler handles AMAZON.HelpIntent" {
|
||||
const allocator = std.testing.allocator;
|
||||
const help_request =
|
||||
\\{"request":{"type":"IntentRequest","intent":{"name":"AMAZON.HelpIntent"}}}
|
||||
;
|
||||
const response = try handler(allocator, help_request);
|
||||
defer allocator.free(response);
|
||||
|
||||
const parsed = try json.parseFromSlice(json.Value, allocator, response, .{});
|
||||
defer parsed.deinit();
|
||||
|
||||
const resp = parsed.value.object.get("response").?.object;
|
||||
try std.testing.expect(resp.get("shouldEndSession").?.bool == false);
|
||||
const text = resp.get("outputSpeech").?.object.get("text").?.string;
|
||||
try std.testing.expect(std.mem.indexOf(u8, text, "recirculation") != null);
|
||||
}
|
||||
|
||||
test "handler handles AMAZON.StopIntent" {
|
||||
const allocator = std.testing.allocator;
|
||||
const stop_request =
|
||||
\\{"request":{"type":"IntentRequest","intent":{"name":"AMAZON.StopIntent"}}}
|
||||
;
|
||||
const response = try handler(allocator, stop_request);
|
||||
defer allocator.free(response);
|
||||
|
||||
const parsed = try json.parseFromSlice(json.Value, allocator, response, .{});
|
||||
defer parsed.deinit();
|
||||
|
||||
const resp = parsed.value.object.get("response").?.object;
|
||||
try std.testing.expect(resp.get("shouldEndSession").?.bool == true);
|
||||
const text = resp.get("outputSpeech").?.object.get("text").?.string;
|
||||
try std.testing.expectEqualStrings("Okay, goodbye.", text);
|
||||
}
|
||||
|
||||
test "handler handles AMAZON.CancelIntent" {
|
||||
const allocator = std.testing.allocator;
|
||||
const cancel_request =
|
||||
\\{"request":{"type":"IntentRequest","intent":{"name":"AMAZON.CancelIntent"}}}
|
||||
;
|
||||
const response = try handler(allocator, cancel_request);
|
||||
defer allocator.free(response);
|
||||
|
||||
const parsed = try json.parseFromSlice(json.Value, allocator, response, .{});
|
||||
defer parsed.deinit();
|
||||
|
||||
const resp = parsed.value.object.get("response").?.object;
|
||||
try std.testing.expect(resp.get("shouldEndSession").?.bool == true);
|
||||
}
|
||||
|
||||
test "handler handles unknown intent" {
|
||||
const allocator = std.testing.allocator;
|
||||
const unknown_intent =
|
||||
\\{"request":{"type":"IntentRequest","intent":{"name":"SomeRandomIntent"}}}
|
||||
;
|
||||
const response = try handler(allocator, unknown_intent);
|
||||
defer allocator.free(response);
|
||||
|
||||
const parsed = try json.parseFromSlice(json.Value, allocator, response, .{});
|
||||
defer parsed.deinit();
|
||||
|
||||
const text = parsed.value.object.get("response").?.object.get("outputSpeech").?.object.get("text").?.string;
|
||||
try std.testing.expectEqualStrings("I don't know how to do that.", text);
|
||||
}
|
||||
|
||||
test "handler handles unknown request type" {
|
||||
const allocator = std.testing.allocator;
|
||||
const unknown_request =
|
||||
\\{"request":{"type":"SomeOtherRequest"}}
|
||||
;
|
||||
const response = try handler(allocator, unknown_request);
|
||||
defer allocator.free(response);
|
||||
|
||||
const parsed = try json.parseFromSlice(json.Value, allocator, response, .{});
|
||||
defer parsed.deinit();
|
||||
|
||||
const text = parsed.value.object.get("response").?.object.get("outputSpeech").?.object.get("text").?.string;
|
||||
try std.testing.expectEqualStrings("I didn't understand that.", text);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue