* refactor: add acpx runtime embedding API * fix: preserve session state on replay failure * fix: honor startup controls in runtime manager * Runtime: deduplicate shared session engine * Runtime: honor oneshot and steer inputs * Runtime: keep persistent sessions stable * Runtime: initialize embedded event logs * Runtime: drop unsupported ensure env * Refactor: reorganize ACPX source layout * refactor: finish ACPX source layout cleanup * docs: note runtime embedding API
171 lines
4.9 KiB
TypeScript
171 lines
4.9 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import {
|
|
exitCodeForOutputErrorCode,
|
|
normalizeOutputError,
|
|
isAcpQueryClosedBeforeResponseError,
|
|
isAcpResourceNotFoundError,
|
|
} from "../src/acp/error-normalization.js";
|
|
import {
|
|
PermissionPromptUnavailableError,
|
|
QueueConnectionError,
|
|
AuthPolicyError,
|
|
} from "../src/errors.js";
|
|
|
|
test("normalizeOutputError maps permission prompt unavailable errors", () => {
|
|
const normalized = normalizeOutputError(new PermissionPromptUnavailableError(), {
|
|
origin: "runtime",
|
|
});
|
|
|
|
assert.equal(normalized.code, "PERMISSION_PROMPT_UNAVAILABLE");
|
|
assert.equal(normalized.origin, "runtime");
|
|
assert.match(normalized.message, /Permission prompt unavailable/i);
|
|
});
|
|
|
|
test("normalizeOutputError maps ACP resource not found errors to NO_SESSION", () => {
|
|
const error = {
|
|
code: -32002,
|
|
message: "Resource not found: session",
|
|
data: {
|
|
sessionId: "abc",
|
|
},
|
|
};
|
|
|
|
const normalized = normalizeOutputError(error, {
|
|
origin: "acp",
|
|
});
|
|
|
|
assert.equal(normalized.code, "NO_SESSION");
|
|
assert.equal(normalized.origin, "acp");
|
|
assert.deepEqual(normalized.acp, {
|
|
code: -32002,
|
|
message: "Resource not found: session",
|
|
data: {
|
|
sessionId: "abc",
|
|
},
|
|
});
|
|
assert.equal(isAcpResourceNotFoundError(error), true);
|
|
});
|
|
|
|
test("isAcpResourceNotFoundError recognizes session-not-found hints in nested errors", () => {
|
|
assert.equal(
|
|
isAcpResourceNotFoundError({
|
|
cause: {
|
|
message: "session not found while reconnecting",
|
|
},
|
|
}),
|
|
true,
|
|
);
|
|
});
|
|
|
|
test("isAcpResourceNotFoundError recognizes Cursor session-not-found format", () => {
|
|
// Cursor returns: {"code":-32602,"message":"Invalid params","data":{"message":"Session \"xxx\" not found"}}
|
|
const cursorError = {
|
|
code: -32602,
|
|
message: "Invalid params",
|
|
data: {
|
|
message: 'Session "nonexistent-session-id" not found',
|
|
},
|
|
};
|
|
|
|
assert.equal(isAcpResourceNotFoundError(cursorError), true);
|
|
});
|
|
test("isAcpQueryClosedBeforeResponseError matches typed ACP payload", () => {
|
|
const error = {
|
|
code: -32603,
|
|
message: "Internal error",
|
|
data: {
|
|
details: "Query closed before response received",
|
|
},
|
|
};
|
|
|
|
assert.equal(isAcpQueryClosedBeforeResponseError(error), true);
|
|
});
|
|
|
|
test("isAcpQueryClosedBeforeResponseError ignores unrelated ACP errors", () => {
|
|
const error = {
|
|
code: -32603,
|
|
message: "Internal error",
|
|
data: {
|
|
details: "other detail",
|
|
},
|
|
};
|
|
|
|
assert.equal(isAcpQueryClosedBeforeResponseError(error), false);
|
|
});
|
|
|
|
test("normalizeOutputError preserves queue metadata from typed queue errors", () => {
|
|
const error = new QueueConnectionError("Queue denied control request", {
|
|
outputCode: "PERMISSION_DENIED",
|
|
detailCode: "QUEUE_CONTROL_REQUEST_FAILED",
|
|
origin: "queue",
|
|
retryable: false,
|
|
});
|
|
|
|
const normalized = normalizeOutputError(error);
|
|
assert.equal(normalized.code, "PERMISSION_DENIED");
|
|
assert.equal(normalized.detailCode, "QUEUE_CONTROL_REQUEST_FAILED");
|
|
assert.equal(normalized.origin, "queue");
|
|
assert.equal(normalized.retryable, false);
|
|
});
|
|
|
|
test("normalizeOutputError maps AuthPolicyError to AUTH_REQUIRED detail", () => {
|
|
const normalized = normalizeOutputError(
|
|
new AuthPolicyError("missing credentials for auth method token"),
|
|
);
|
|
|
|
assert.equal(normalized.code, "RUNTIME");
|
|
assert.equal(normalized.detailCode, "AUTH_REQUIRED");
|
|
assert.equal(normalized.origin, "acp");
|
|
});
|
|
|
|
test("normalizeOutputError infers AUTH_REQUIRED detail from ACP payload", () => {
|
|
const normalized = normalizeOutputError({
|
|
error: {
|
|
code: -32000,
|
|
message: "Authentication required",
|
|
data: {
|
|
methodId: "token",
|
|
},
|
|
},
|
|
});
|
|
|
|
assert.equal(normalized.code, "RUNTIME");
|
|
assert.equal(normalized.detailCode, "AUTH_REQUIRED");
|
|
assert.equal(normalized.acp?.code, -32000);
|
|
});
|
|
|
|
test("normalizeOutputError extracts ACP payload from wrapped errors", () => {
|
|
const wrapped = new Error("Agent rejected session/set_mode");
|
|
(
|
|
wrapped as Error & {
|
|
acp?: { code: number; message: string; data?: unknown };
|
|
}
|
|
).acp = {
|
|
code: -32602,
|
|
message: "Invalid params",
|
|
data: {
|
|
method: "session/set_mode",
|
|
modeId: "plan",
|
|
},
|
|
};
|
|
|
|
const normalized = normalizeOutputError(wrapped);
|
|
|
|
assert.equal(normalized.code, "RUNTIME");
|
|
assert.equal(normalized.acp?.code, -32602);
|
|
assert.deepEqual(normalized.acp?.data, {
|
|
method: "session/set_mode",
|
|
modeId: "plan",
|
|
});
|
|
});
|
|
|
|
test("exitCodeForOutputErrorCode maps machine codes to stable exits", () => {
|
|
assert.equal(exitCodeForOutputErrorCode("USAGE"), 2);
|
|
assert.equal(exitCodeForOutputErrorCode("TIMEOUT"), 3);
|
|
assert.equal(exitCodeForOutputErrorCode("NO_SESSION"), 4);
|
|
assert.equal(exitCodeForOutputErrorCode("PERMISSION_DENIED"), 5);
|
|
assert.equal(exitCodeForOutputErrorCode("PERMISSION_PROMPT_UNAVAILABLE"), 5);
|
|
assert.equal(exitCodeForOutputErrorCode("RUNTIME"), 1);
|
|
});
|