mirror of
https://github.com/coder/registry.git
synced 2026-06-02 20:48:14 +00:00
feat(coder/modules/agentapi): add log snapshot capture on shutdown (#676)
Captures the last 10 messages from AgentAPI when task workspaces stop, allowing users to view conversation history while the task is paused. The shutdown script fetches messages, builds a payload with last 10 messages, truncates to 64KB if needed (removes old messages first, then truncates content of the last message), and posts to the log snapshot endpoint. Gracefully handles non-task workspaces (skips), older Coder versions without the endpoint (logs and continues), and empty message sets. Enabled by default via task_log_snapshot variable. Task ID is automatically resolved from data.coder_task when available. Updates coder/internal#1257
This commit is contained in:
committed by
GitHub
parent
e4606c51f3
commit
fa30191394
@@ -16,7 +16,7 @@ The AgentAPI module is a building block for modules that need to run an AgentAPI
|
||||
```tf
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "2.0.0"
|
||||
version = "2.1.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
@@ -49,6 +49,19 @@ module "agentapi" {
|
||||
}
|
||||
```
|
||||
|
||||
## Task log snapshot
|
||||
|
||||
Captures the last 10 messages from AgentAPI when a task workspace stops. This allows viewing conversation history while the task is paused.
|
||||
|
||||
To enable for task workspaces:
|
||||
|
||||
```tf
|
||||
module "agentapi" {
|
||||
# ... other config
|
||||
task_log_snapshot = true # default: true
|
||||
}
|
||||
```
|
||||
|
||||
## For module developers
|
||||
|
||||
For a complete example of how to use this module, see the [Goose module](https://github.com/coder/registry/blob/main/registry/coder/modules/goose/main.tf).
|
||||
|
||||
@@ -257,4 +257,157 @@ describe("agentapi", async () => {
|
||||
);
|
||||
expect(agentApiStartLog).toContain("AGENTAPI_ALLOWED_HOSTS: *");
|
||||
});
|
||||
|
||||
describe("shutdown script", async () => {
|
||||
const setupMocks = async (
|
||||
containerId: string,
|
||||
agentapiPreset: string,
|
||||
httpCode: number = 204,
|
||||
) => {
|
||||
const agentapiMock = await loadTestFile(
|
||||
import.meta.dir,
|
||||
"agentapi-mock-shutdown.js",
|
||||
);
|
||||
const coderMock = await loadTestFile(
|
||||
import.meta.dir,
|
||||
"coder-instance-mock.js",
|
||||
);
|
||||
|
||||
await writeExecutable({
|
||||
containerId,
|
||||
filePath: "/usr/local/bin/mock-agentapi",
|
||||
content: agentapiMock,
|
||||
});
|
||||
|
||||
await writeExecutable({
|
||||
containerId,
|
||||
filePath: "/usr/local/bin/mock-coder",
|
||||
content: coderMock,
|
||||
});
|
||||
|
||||
await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
`PRESET=${agentapiPreset} nohup node /usr/local/bin/mock-agentapi 3284 > /tmp/mock-agentapi.log 2>&1 &`,
|
||||
]);
|
||||
|
||||
await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
`HTTP_CODE=${httpCode} nohup node /usr/local/bin/mock-coder 18080 > /tmp/mock-coder.log 2>&1 &`,
|
||||
]);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
};
|
||||
|
||||
const runShutdownScript = async (
|
||||
containerId: string,
|
||||
taskId: string = "test-task",
|
||||
) => {
|
||||
const shutdownScript = await loadTestFile(
|
||||
import.meta.dir,
|
||||
"../scripts/agentapi-shutdown.sh",
|
||||
);
|
||||
|
||||
await writeExecutable({
|
||||
containerId,
|
||||
filePath: "/tmp/shutdown.sh",
|
||||
content: shutdownScript,
|
||||
});
|
||||
|
||||
return await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
`ARG_TASK_ID=${taskId} ARG_AGENTAPI_PORT=3284 CODER_AGENT_URL=http://localhost:18080 CODER_AGENT_TOKEN=test-token /tmp/shutdown.sh`,
|
||||
]);
|
||||
};
|
||||
|
||||
test("posts snapshot with normal messages", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {},
|
||||
skipAgentAPIMock: true,
|
||||
});
|
||||
|
||||
await setupMocks(id, "normal");
|
||||
const result = await runShutdownScript(id);
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("Retrieved 5 messages for log snapshot");
|
||||
expect(result.stdout).toContain("Log snapshot posted successfully");
|
||||
|
||||
const posted = await readFileContainer(id, "/tmp/snapshot-posted.json");
|
||||
const snapshot = JSON.parse(posted);
|
||||
expect(snapshot.task_id).toBe("test-task");
|
||||
expect(snapshot.payload.messages).toHaveLength(5);
|
||||
expect(snapshot.payload.messages[0].content).toBe("Hello");
|
||||
expect(snapshot.payload.messages[4].content).toBe("Great");
|
||||
});
|
||||
|
||||
test("truncates to last 10 messages", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {},
|
||||
skipAgentAPIMock: true,
|
||||
});
|
||||
|
||||
await setupMocks(id, "many");
|
||||
const result = await runShutdownScript(id);
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
|
||||
const posted = await readFileContainer(id, "/tmp/snapshot-posted.json");
|
||||
const snapshot = JSON.parse(posted);
|
||||
expect(snapshot.task_id).toBe("test-task");
|
||||
expect(snapshot.payload.messages).toHaveLength(10);
|
||||
expect(snapshot.payload.messages[0].content).toBe("Message 6");
|
||||
expect(snapshot.payload.messages[9].content).toBe("Message 15");
|
||||
});
|
||||
|
||||
test("truncates huge message content", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {},
|
||||
skipAgentAPIMock: true,
|
||||
});
|
||||
|
||||
await setupMocks(id, "huge");
|
||||
const result = await runShutdownScript(id);
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("truncating final message content");
|
||||
|
||||
const posted = await readFileContainer(id, "/tmp/snapshot-posted.json");
|
||||
const snapshot = JSON.parse(posted);
|
||||
expect(snapshot.task_id).toBe("test-task");
|
||||
expect(snapshot.payload.messages).toHaveLength(1);
|
||||
expect(snapshot.payload.messages[0].content).toContain(
|
||||
"[...content truncated",
|
||||
);
|
||||
});
|
||||
|
||||
test("skips gracefully when TASK_ID is empty", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {},
|
||||
skipAgentAPIMock: true,
|
||||
});
|
||||
|
||||
const result = await runShutdownScript(id, "");
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("No task ID, skipping log snapshot");
|
||||
});
|
||||
|
||||
test("handles 404 gracefully for older Coder versions", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {},
|
||||
skipAgentAPIMock: true,
|
||||
});
|
||||
|
||||
await setupMocks(id, "normal", 404);
|
||||
const result = await runShutdownScript(id);
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain(
|
||||
"Log snapshot endpoint not supported by this Coder version",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.12"
|
||||
version = ">= 2.13"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ data "coder_workspace" "me" {}
|
||||
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
data "coder_task" "me" {}
|
||||
|
||||
variable "web_app_order" {
|
||||
type = number
|
||||
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
|
||||
@@ -126,6 +128,12 @@ variable "agentapi_port" {
|
||||
default = 3284
|
||||
}
|
||||
|
||||
variable "task_log_snapshot" {
|
||||
type = bool
|
||||
description = "Capture last 10 messages when workspace stops for offline viewing while task is paused."
|
||||
default = true
|
||||
}
|
||||
|
||||
locals {
|
||||
# agentapi_subdomain_false_min_version_expr matches a semantic version >= v0.3.3.
|
||||
# Initial support was added in v0.3.1 but configuration via environment variable
|
||||
@@ -173,6 +181,7 @@ locals {
|
||||
// for backward compatibility.
|
||||
agentapi_chat_base_path = var.agentapi_subdomain ? "" : "/@${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}.${var.agent_id}/apps/${var.web_app_slug}/chat"
|
||||
main_script = file("${path.module}/scripts/main.sh")
|
||||
shutdown_script = file("${path.module}/scripts/agentapi-shutdown.sh")
|
||||
}
|
||||
|
||||
resource "coder_script" "agentapi" {
|
||||
@@ -198,11 +207,32 @@ resource "coder_script" "agentapi" {
|
||||
ARG_POST_INSTALL_SCRIPT="$(echo -n '${local.encoded_post_install_script}' | base64 -d)" \
|
||||
ARG_AGENTAPI_PORT='${var.agentapi_port}' \
|
||||
ARG_AGENTAPI_CHAT_BASE_PATH='${local.agentapi_chat_base_path}' \
|
||||
ARG_TASK_ID='${try(data.coder_task.me.id, "")}' \
|
||||
ARG_TASK_LOG_SNAPSHOT='${var.task_log_snapshot}' \
|
||||
/tmp/main.sh
|
||||
EOT
|
||||
run_on_start = true
|
||||
}
|
||||
|
||||
resource "coder_script" "agentapi_shutdown" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "AgentAPI Shutdown"
|
||||
icon = var.web_app_icon
|
||||
run_on_stop = true
|
||||
script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -o pipefail
|
||||
|
||||
echo -n '${base64encode(local.shutdown_script)}' | base64 -d > /tmp/agentapi-shutdown.sh
|
||||
chmod +x /tmp/agentapi-shutdown.sh
|
||||
|
||||
ARG_TASK_ID='${try(data.coder_task.me.id, "")}' \
|
||||
ARG_TASK_LOG_SNAPSHOT='${var.task_log_snapshot}' \
|
||||
ARG_AGENTAPI_PORT='${var.agentapi_port}' \
|
||||
/tmp/agentapi-shutdown.sh
|
||||
EOT
|
||||
}
|
||||
|
||||
resource "coder_app" "agentapi_web" {
|
||||
slug = var.web_app_slug
|
||||
display_name = var.web_app_display_name
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
#!/usr/bin/env bash
|
||||
# AgentAPI shutdown script.
|
||||
#
|
||||
# Captures the last 10 messages from AgentAPI and posts them to Coder instance
|
||||
# as a snapshot. This script is called during workspace shutdown to access
|
||||
# conversation history for paused tasks.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Configuration (set via Terraform interpolation).
|
||||
readonly TASK_ID="${ARG_TASK_ID:-}"
|
||||
readonly TASK_LOG_SNAPSHOT="${ARG_TASK_LOG_SNAPSHOT:-true}"
|
||||
readonly AGENTAPI_PORT="${ARG_AGENTAPI_PORT:-3284}"
|
||||
|
||||
# Runtime environment variables.
|
||||
readonly CODER_AGENT_URL="${CODER_AGENT_URL:-}"
|
||||
readonly CODER_AGENT_TOKEN="${CODER_AGENT_TOKEN:-}"
|
||||
|
||||
# Constants.
|
||||
readonly MAX_PAYLOAD_SIZE=65536 # 64KB
|
||||
readonly MAX_MESSAGE_CONTENT=57344 # 56KB
|
||||
readonly MAX_MESSAGES=10
|
||||
readonly FETCH_TIMEOUT=5
|
||||
readonly POST_TIMEOUT=10
|
||||
|
||||
log() {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
error() {
|
||||
echo "Error: $*" >&2
|
||||
}
|
||||
|
||||
fetch_and_build_messages_payload() {
|
||||
local payload_file="$1"
|
||||
local messages_url="http://localhost:${AGENTAPI_PORT}/messages"
|
||||
|
||||
log "Fetching messages from AgentAPI on port $AGENTAPI_PORT"
|
||||
|
||||
if ! curl -fsSL --max-time "$FETCH_TIMEOUT" "$messages_url" > "$payload_file"; then
|
||||
error "Failed to fetch messages from AgentAPI (may not be running)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Update messages field to keep only last N messages.
|
||||
if ! jq --argjson n "$MAX_MESSAGES" '.messages |= .[-$n:]' < "$payload_file" > "${payload_file}.tmp"; then
|
||||
error "Failed to select last $MAX_MESSAGES messages"
|
||||
return 1
|
||||
fi
|
||||
mv "${payload_file}.tmp" "$payload_file"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
truncate_messages_payload_to_size() {
|
||||
local payload_file="$1"
|
||||
local max_size="$2"
|
||||
|
||||
while true; do
|
||||
local size
|
||||
size=$(wc -c < "$payload_file")
|
||||
|
||||
if ((size <= max_size)); then
|
||||
break
|
||||
fi
|
||||
|
||||
local count
|
||||
count=$(jq '.messages | length' < "$payload_file")
|
||||
|
||||
if ((count == 1)); then
|
||||
# Down to last message, truncate its content keeping the tail.
|
||||
log "Payload size $size bytes exceeds limit, truncating final message content"
|
||||
|
||||
# Keep tail of content with truncation indicator, leaving room for JSON
|
||||
# overhead.
|
||||
if ! jq --argjson maxlen "$MAX_MESSAGE_CONTENT" '.messages[0].content |= (if length > $maxlen then "[...content truncated, showing last 56KB...]\n\n" + .[-$maxlen:] else . end)' < "$payload_file" > "${payload_file}.tmp"; then
|
||||
error "Failed to truncate message content"
|
||||
return 1
|
||||
fi
|
||||
mv "${payload_file}.tmp" "$payload_file"
|
||||
|
||||
# Verify the truncation was sufficient.
|
||||
size=$(wc -c < "$payload_file")
|
||||
if ((size > max_size)); then
|
||||
error "Payload still too large after content truncation, giving up"
|
||||
return 1
|
||||
fi
|
||||
break
|
||||
else
|
||||
# More than one message, remove the oldest.
|
||||
log "Payload size $size bytes exceeds limit, removing oldest message"
|
||||
|
||||
if ! jq '.messages |= .[1:]' < "$payload_file" > "${payload_file}.tmp"; then
|
||||
error "Failed to remove oldest message"
|
||||
return 1
|
||||
fi
|
||||
mv "${payload_file}.tmp" "$payload_file"
|
||||
fi
|
||||
done
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
post_task_log_snapshot() {
|
||||
local payload_file="$1"
|
||||
local tmpdir="$2"
|
||||
|
||||
local snapshot_url="${CODER_AGENT_URL}/api/v2/workspaceagents/me/tasks/${TASK_ID}/log-snapshot?format=agentapi"
|
||||
local response_file="${tmpdir}/response.txt"
|
||||
|
||||
log "Posting log snapshot to Coder instance"
|
||||
|
||||
local http_code
|
||||
if ! http_code=$(curl -sS -w "%{http_code}" -o "$response_file" \
|
||||
--max-time "$POST_TIMEOUT" \
|
||||
-X POST "$snapshot_url" \
|
||||
-H "Coder-Session-Token: $CODER_AGENT_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data-binary "@$payload_file"); then
|
||||
error "Failed to connect to Coder instance (curl failed)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ $http_code == 204 ]]; then
|
||||
log "Log snapshot posted successfully"
|
||||
return 0
|
||||
elif [[ $http_code == 404 ]]; then
|
||||
log "Log snapshot endpoint not supported by this Coder version, skipping"
|
||||
return 0
|
||||
else
|
||||
local response
|
||||
response=$(cat "$response_file" 2> /dev/null || echo "")
|
||||
error "Failed to post log snapshot (HTTP $http_code): $response"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
capture_task_log_snapshot() {
|
||||
if [[ -z $TASK_ID ]]; then
|
||||
log "No task ID, skipping log snapshot"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ -z $CODER_AGENT_URL ]]; then
|
||||
error "CODER_AGENT_URL not set, cannot capture log snapshot"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z $CODER_AGENT_TOKEN ]]; then
|
||||
error "CODER_AGENT_TOKEN not set, cannot capture log snapshot"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v jq > /dev/null 2>&1; then
|
||||
error "jq not found, cannot capture log snapshot"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v curl > /dev/null 2>&1; then
|
||||
error "curl not found, cannot capture log snapshot"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tmpdir=$(mktemp -d)
|
||||
trap 'rm -rf "$tmpdir"' EXIT
|
||||
|
||||
local payload_file="${tmpdir}/payload.json"
|
||||
|
||||
if ! fetch_and_build_messages_payload "$payload_file"; then
|
||||
error "Cannot capture log snapshot without messages"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local message_count
|
||||
message_count=$(jq '.messages | length' < "$payload_file")
|
||||
if ((message_count == 0)); then
|
||||
log "No messages for log snapshot"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log "Retrieved $message_count messages for log snapshot"
|
||||
|
||||
# Ensure payload fits within size limit.
|
||||
if ! truncate_messages_payload_to_size "$payload_file" "$MAX_PAYLOAD_SIZE"; then
|
||||
error "Failed to truncate payload to size limit"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local final_size final_count
|
||||
final_size=$(wc -c < "$payload_file")
|
||||
final_count=$(jq '.messages | length' < "$payload_file")
|
||||
log "Log snapshot payload: $final_size bytes, $final_count messages"
|
||||
|
||||
if ! post_task_log_snapshot "$payload_file" "$tmpdir"; then
|
||||
error "Log snapshot capture failed"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
log "Shutting down AgentAPI"
|
||||
|
||||
if [[ $TASK_LOG_SNAPSHOT == true ]]; then
|
||||
capture_task_log_snapshot
|
||||
else
|
||||
log "Log snapshot disabled, skipping"
|
||||
fi
|
||||
|
||||
log "Shutdown complete"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -14,6 +14,8 @@ WAIT_FOR_START_SCRIPT="$ARG_WAIT_FOR_START_SCRIPT"
|
||||
POST_INSTALL_SCRIPT="$ARG_POST_INSTALL_SCRIPT"
|
||||
AGENTAPI_PORT="$ARG_AGENTAPI_PORT"
|
||||
AGENTAPI_CHAT_BASE_PATH="${ARG_AGENTAPI_CHAT_BASE_PATH:-}"
|
||||
TASK_ID="${ARG_TASK_ID:-}"
|
||||
TASK_LOG_SNAPSHOT="${ARG_TASK_LOG_SNAPSHOT:-true}"
|
||||
set +o nounset
|
||||
|
||||
command_exists() {
|
||||
@@ -23,6 +25,13 @@ command_exists() {
|
||||
module_path="$HOME/${MODULE_DIR_NAME}"
|
||||
mkdir -p "$module_path/scripts"
|
||||
|
||||
# Check for jq dependency if task log snapshot is enabled.
|
||||
if [[ $TASK_LOG_SNAPSHOT == true ]] && [[ -n $TASK_ID ]]; then
|
||||
if ! command_exists jq; then
|
||||
echo "Warning: jq is not installed. Task log snapshot requires jq to capture conversation history."
|
||||
echo "Install jq to enable log snapshot functionality when the workspace stops."
|
||||
fi
|
||||
fi
|
||||
if [ ! -d "${WORKDIR}" ]; then
|
||||
echo "Warning: The specified folder '${WORKDIR}' does not exist."
|
||||
echo "Creating the folder..."
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env node
|
||||
// Mock AgentAPI server for shutdown script tests.
|
||||
// Usage: MESSAGES='[...]' node agentapi-mock-shutdown.js [port]
|
||||
|
||||
const http = require("http");
|
||||
const port = process.argv[2] || 3284;
|
||||
|
||||
// Parse messages from environment or use default
|
||||
let messages = [];
|
||||
if (process.env.MESSAGES) {
|
||||
try {
|
||||
messages = JSON.parse(process.env.MESSAGES);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse MESSAGES env var:", e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Presets for common test scenarios
|
||||
if (process.env.PRESET === "normal") {
|
||||
messages = [
|
||||
{ id: 1, type: "input", content: "Hello", time: "2025-01-01T00:00:00Z" },
|
||||
{
|
||||
id: 2,
|
||||
type: "output",
|
||||
content: "Hi there",
|
||||
time: "2025-01-01T00:00:01Z",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: "input",
|
||||
content: "How are you?",
|
||||
time: "2025-01-01T00:00:02Z",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
type: "output",
|
||||
content: "Good!",
|
||||
time: "2025-01-01T00:00:03Z",
|
||||
},
|
||||
{ id: 5, type: "input", content: "Great", time: "2025-01-01T00:00:04Z" },
|
||||
];
|
||||
} else if (process.env.PRESET === "many") {
|
||||
messages = Array.from({ length: 15 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
type: "input",
|
||||
content: `Message ${i + 1}`,
|
||||
time: "2025-01-01T00:00:00Z",
|
||||
}));
|
||||
} else if (process.env.PRESET === "huge") {
|
||||
messages = [
|
||||
{
|
||||
id: 1,
|
||||
type: "output",
|
||||
content: "x".repeat(70000),
|
||||
time: "2025-01-01T00:00:00Z",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
if (req.url === "/messages") {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ messages }));
|
||||
} else if (req.url === "/status") {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ status: "stable" }));
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(port, () => {
|
||||
console.error(`Mock AgentAPI listening on port ${port}`);
|
||||
});
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
server.close(() => process.exit(0));
|
||||
});
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
server.close(() => process.exit(0));
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env node
|
||||
// Mock Coder instance server for shutdown script tests.
|
||||
// Captures POST requests to /log-snapshot endpoint.
|
||||
|
||||
const http = require("http");
|
||||
const fs = require("fs");
|
||||
const port = process.argv[2] || 8080;
|
||||
const outputFile = process.env.OUTPUT_FILE || "/tmp/snapshot-posted.json";
|
||||
const httpCode = parseInt(process.env.HTTP_CODE || "204", 10);
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
const url = new URL(req.url, `http://localhost:${port}`);
|
||||
|
||||
// Expected path: /api/v2/workspaceagents/me/tasks/{task_id}/log-snapshot
|
||||
const pathMatch = url.pathname.match(/\/tasks\/([^\/]+)\/log-snapshot$/);
|
||||
|
||||
if (req.method === "POST" && pathMatch) {
|
||||
const taskId = pathMatch[1];
|
||||
let body = "";
|
||||
req.on("data", (chunk) => {
|
||||
body += chunk.toString();
|
||||
});
|
||||
|
||||
req.on("end", () => {
|
||||
// Save captured snapshot with task ID for verification
|
||||
const snapshotData = {
|
||||
task_id: taskId,
|
||||
payload: JSON.parse(body),
|
||||
};
|
||||
fs.writeFileSync(outputFile, JSON.stringify(snapshotData, null, 2));
|
||||
console.error(
|
||||
`Captured snapshot for task ${taskId} (${body.length} bytes) to ${outputFile}`,
|
||||
);
|
||||
|
||||
// Return configured status code
|
||||
res.writeHead(httpCode);
|
||||
res.end();
|
||||
});
|
||||
|
||||
req.on("error", (err) => {
|
||||
console.error("Request error:", err);
|
||||
res.writeHead(500);
|
||||
res.end();
|
||||
});
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(port, () => {
|
||||
console.error(`Mock Coder instance listening on port ${port}`);
|
||||
});
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
server.close(() => process.exit(0));
|
||||
});
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
server.close(() => process.exit(0));
|
||||
});
|
||||
Reference in New Issue
Block a user