@intentius/chant-lexicon-aws 0.44.10 → 0.44.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agentcore/trace-fetch.d.ts +332 -0
- package/dist/agentcore/trace-fetch.d.ts.map +1 -0
- package/dist/agentcore/trace-render.d.ts +256 -0
- package/dist/agentcore/trace-render.d.ts.map +1 -0
- package/dist/api/read-client.d.ts +16 -0
- package/dist/api/read-client.d.ts.map +1 -1
- package/dist/identity-observe.d.ts.map +1 -1
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/dist/op/activities/index.d.ts +10 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/agentcore/trace-fetch.test.ts +588 -0
- package/src/agentcore/trace-fetch.ts +754 -0
- package/src/agentcore/trace-render.test.ts +366 -0
- package/src/agentcore/trace-render.ts +589 -0
- package/src/api/read-client.ts +7 -1
- package/src/identity-observe.test.ts +44 -2
- package/src/identity-observe.ts +25 -2
- package/src/op/activities/index.ts +15 -0
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "ac39a61eac75584e4bba1421dc2ce6378390c5f59c4a057108589654078e8cb0",
|
|
5
5
|
"meta.json": "2e4cae67cbc2eb2962c3dd0aa57008ae96fb725dd0118af7b04b2b44eab7722a",
|
|
6
6
|
"types/index.d.ts": "6d6b37bd14a9b40fb3ff5c66e1cb49be1ae1032de47a4e167cf5f411c943b7f5",
|
|
7
7
|
"rules/hardcoded-region.ts": "5a0eaf7ab391231fe6cd51426ece29539cb4b36f31c8dd060956638fed55722a",
|
|
@@ -62,5 +62,5 @@
|
|
|
62
62
|
"skills/chant-aws-eks.md": "8789255709ff004ad0a875fd5999edcdc66fc6e33d710db058d9f42703bcfdfe",
|
|
63
63
|
"skills/chant-aws-carve-terraform.md": "52dc7a8a10156d236266cc8dbf75614fabb6e2fa4caf31c13cca348e167a58b5"
|
|
64
64
|
},
|
|
65
|
-
"composite": "
|
|
65
|
+
"composite": "aa1c18dc2a4bdd53abaf88777f10d94dec5481f91c8750fea5c9b90d168ae02a"
|
|
66
66
|
}
|
package/dist/manifest.json
CHANGED
|
@@ -4,9 +4,19 @@
|
|
|
4
4
|
* emulator lifecycle (`flociUp`/`flociDown`) and the native CloudFormation
|
|
5
5
|
* applier (`awsApply`), which calls the CloudFormation API directly rather than
|
|
6
6
|
* shelling `aws` — the direct twin of `azApply`/`gcpApply`.
|
|
7
|
+
*
|
|
8
|
+
* The registry keys every exported *function* here by its name, so only the
|
|
9
|
+
* activities themselves belong in this barrel. `awsAgentCoreFetchTrace`'s
|
|
10
|
+
* helpers — the normalizer, the JSON coercion, the `ListEvents` walk — stay
|
|
11
|
+
* importable from `@intentius/chant-lexicon-aws/agentcore/trace-fetch` rather
|
|
12
|
+
* than being registered as activities nobody would ever name in a step, and the
|
|
13
|
+
* pure renderer lives one module further out again, in
|
|
14
|
+
* `@intentius/chant-lexicon-aws/agentcore/trace-render`.
|
|
7
15
|
*/
|
|
8
16
|
export { flociUp, flociDown, flociRunCommand, flociRmCommand, flociExistsCommand, flociHealthUrl, flociEnv, isFlociReady, } from "./floci.js";
|
|
9
17
|
export type { FlociUpArgs, FlociDownArgs } from "./floci.js";
|
|
10
18
|
export { awsApply, awsDelete, waitForStackSettled, cfnUrl, cfnForm, capabilityParams, xmlField, stackStatus, stackId, cfnErrorMessage, isStackMissing, isNoUpdates, isSuccessStatus, isFailureStatus, isTerminalStatus, } from "./aws-apply.js";
|
|
11
19
|
export type { AwsApplyArgs, AwsHttp } from "./aws-apply.js";
|
|
20
|
+
export { awsAgentCoreFetchTrace } from "../../agentcore/trace-fetch.js";
|
|
21
|
+
export type { AgentCoreTraceSource, AwsAgentCoreFetchTraceArgs, AwsAgentCoreFetchTraceResult, } from "../../agentcore/trace-fetch.js";
|
|
12
22
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/op/activities/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/op/activities/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EACL,OAAO,EACP,SAAS,EACT,eAAe,EACf,cAAc,EACd,kBAAkB,EAClB,cAAc,EACd,QAAQ,EACR,YAAY,GACb,MAAM,SAAS,CAAC;AACjB,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE1D,OAAO,EACL,QAAQ,EACR,SAAS,EACT,mBAAmB,EACnB,MAAM,EACN,OAAO,EACP,gBAAgB,EAChB,QAAQ,EACR,WAAW,EACX,OAAO,EACP,eAAe,EACf,cAAc,EACd,WAAW,EACX,eAAe,EACf,eAAe,EACf,gBAAgB,GACjB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAEzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,YAAY,EACV,oBAAoB,EACpB,0BAA0B,EAC1B,4BAA4B,GAC7B,MAAM,6BAA6B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-aws",
|
|
3
|
-
"version": "0.44.
|
|
3
|
+
"version": "0.44.13",
|
|
4
4
|
"description": "AWS CloudFormation lexicon for chant — declarative IaC in TypeScript",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"typescript": "^5.9.3"
|
|
81
81
|
},
|
|
82
82
|
"peerDependencies": {
|
|
83
|
-
"@intentius/chant": "^0.44.
|
|
83
|
+
"@intentius/chant": "^0.44.13",
|
|
84
84
|
"typescript": "^5.9.3"
|
|
85
85
|
}
|
|
86
86
|
}
|
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { mkdtemp, readFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { AwsReadError, type AwsReadHttp } from "../api/read-client";
|
|
6
|
+
import { AgentCoreTraceError, renderValue } from "./trace-render";
|
|
7
|
+
import {
|
|
8
|
+
AgentCoreTraceUnavailableError,
|
|
9
|
+
awsAgentCoreFetchTrace,
|
|
10
|
+
coerceFields,
|
|
11
|
+
decimalText,
|
|
12
|
+
identifierFor,
|
|
13
|
+
listMemoryEvents,
|
|
14
|
+
normalizeMemoryEvents,
|
|
15
|
+
toEpochMs,
|
|
16
|
+
type MemoryEvent,
|
|
17
|
+
} from "./trace-fetch";
|
|
18
|
+
|
|
19
|
+
/** A transport that answers every POST with the next canned body, recording calls. */
|
|
20
|
+
function mockHttp(responses: Array<{ status?: number; body: unknown }>): {
|
|
21
|
+
http: AwsReadHttp;
|
|
22
|
+
calls: Array<{ url: string; body: Record<string, unknown>; headers: Record<string, string> }>;
|
|
23
|
+
} {
|
|
24
|
+
const calls: Array<{ url: string; body: Record<string, unknown>; headers: Record<string, string> }> = [];
|
|
25
|
+
let index = 0;
|
|
26
|
+
const http: AwsReadHttp = async (url, init) => {
|
|
27
|
+
calls.push({ url, body: JSON.parse(init.body) as Record<string, unknown>, headers: init.headers });
|
|
28
|
+
const next = responses[Math.min(index++, responses.length - 1)];
|
|
29
|
+
return { status: next?.status ?? 200, text: JSON.stringify(next?.body ?? {}) };
|
|
30
|
+
};
|
|
31
|
+
return { http, calls };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const BASE = {
|
|
35
|
+
memoryId: "mem-1",
|
|
36
|
+
actorId: "alice",
|
|
37
|
+
sessionId: "sess-1",
|
|
38
|
+
endpoint: "http://localhost:4566",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function memoryEvent(over: Partial<MemoryEvent> = {}): MemoryEvent {
|
|
42
|
+
return {
|
|
43
|
+
eventId: "evt-1",
|
|
44
|
+
sessionId: "sess-1",
|
|
45
|
+
actorId: "alice",
|
|
46
|
+
memoryId: "mem-1",
|
|
47
|
+
eventTimestamp: 1_700_000_000,
|
|
48
|
+
payload: [{ blob: { action: "Transfer", kind: "request", input: { amount: 10 } } }],
|
|
49
|
+
...over,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe("toEpochMs — restJson1 timestamps", () => {
|
|
54
|
+
test("epoch seconds, fractional included", () => {
|
|
55
|
+
expect(toEpochMs(1_700_000_000, "t")).toBe(1_700_000_000_000);
|
|
56
|
+
expect(toEpochMs(1_700_000_000.512, "t")).toBe(1_700_000_000_512);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("a value already in milliseconds is left alone", () => {
|
|
60
|
+
expect(toEpochMs(1_700_000_000_000, "t")).toBe(1_700_000_000_000);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("ISO-8601 strings parse", () => {
|
|
64
|
+
expect(toEpochMs("2023-11-14T22:13:20.000Z", "t")).toBe(1_700_000_000_000);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("anything else throws rather than being guessed at", () => {
|
|
68
|
+
expect(() => toEpochMs(undefined, "an event's timestamp")).toThrow(AgentCoreTraceError);
|
|
69
|
+
expect(() => toEpochMs("not a date", "an event's timestamp")).toThrow(/not a usable timestamp/);
|
|
70
|
+
expect(() => toEpochMs(Number.NaN, "an event's timestamp")).toThrow(/moves every temporal window/);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("coerceFields — arbitrary JSON into trace values", () => {
|
|
75
|
+
test("scalars, nesting and arrays survive", () => {
|
|
76
|
+
const { fields } = coerceFields({ a: 1, b: "x", c: true, d: [1, 2], e: { f: "g" } });
|
|
77
|
+
expect(fields).toEqual({ a: 1, b: "x", c: true, d: [1, 2], e: { f: "g" } });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("a non-integer becomes a Cedar decimal rather than being rounded", () => {
|
|
81
|
+
const { fields } = coerceFields({ price: 1.5 });
|
|
82
|
+
expect(renderValue(fields.price!)).toBe("1.5");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("a number JS would print in exponential form still becomes a decimal literal", () => {
|
|
86
|
+
// String(1e-7) is "1e-7", which is not a decimal literal — that used to
|
|
87
|
+
// abort the whole fetch over a legitimate payload.
|
|
88
|
+
expect(decimalText(1e-7)).toBe("0.0000001");
|
|
89
|
+
expect(decimalText(1.5)).toBe("1.5");
|
|
90
|
+
expect(decimalText(-2.5e-8)).toBe("-0.000000025");
|
|
91
|
+
expect(renderValue(coerceFields({ tiny: 1e-7 }).fields.tiny!)).toBe("0.0000001");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("a value too small to spell exactly throws instead of rounding to 0.0", () => {
|
|
95
|
+
expect(() => coerceFields({ tiny: 1e-25 })).toThrow(/at tiny/);
|
|
96
|
+
expect(() => coerceFields({ tiny: 1e-25 })).toThrow(/a number the agent never saw/);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("null and undefined are dropped from records — Cedar has no null", () => {
|
|
100
|
+
const { fields } = coerceFields({ a: null, b: undefined, c: 1 });
|
|
101
|
+
expect(fields).toEqual({ c: 1 });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("a null inside an array throws, because dropping it would shift every later index", () => {
|
|
105
|
+
expect(() => coerceFields({ args: ["a", null, "c"] })).toThrow(
|
|
106
|
+
/would shift every later element/,
|
|
107
|
+
);
|
|
108
|
+
expect(() => coerceFields({ args: ["a", null] })).toThrow(/args\[1\]/);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("two keys that spell the same identifier throw rather than one overwriting the other", () => {
|
|
112
|
+
expect(() => coerceFields({ "tool-name": 1, "tool.name": 2 })).toThrow(
|
|
113
|
+
/both spell "tool_name"/,
|
|
114
|
+
);
|
|
115
|
+
expect(() => coerceFields({ tool_name: 1, "tool-name": 2 })).toThrow(AgentCoreTraceError);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("a key the grammar cannot spell is rewritten, and the rewrite is reported", () => {
|
|
119
|
+
const { fields, renames } = coerceFields({ "tool-name": "transfer", nested: { "2fa": true } });
|
|
120
|
+
expect(fields).toEqual({ tool_name: "transfer", nested: { f_2fa: true } });
|
|
121
|
+
expect(renames).toEqual([
|
|
122
|
+
{ path: "tool-name", from: "tool-name", to: "tool_name" },
|
|
123
|
+
{ path: "nested.2fa", from: "2fa", to: "f_2fa" },
|
|
124
|
+
]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("onNonIdentifierField: \"fail\" refuses instead of rewriting", () => {
|
|
128
|
+
expect(() => coerceFields({ "tool-name": "x" }, { onNonIdentifierField: "fail" })).toThrow(
|
|
129
|
+
/cannot spell it/,
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("identifierFor is deterministic", () => {
|
|
134
|
+
expect(identifierFor("tool-name")).toBe("tool_name");
|
|
135
|
+
expect(identifierFor("2fa")).toBe("f_2fa");
|
|
136
|
+
expect(identifierFor("ok_name")).toBe("ok_name");
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("normalizeMemoryEvents — ListEvents output into normalized events", () => {
|
|
141
|
+
test("a blob payload maps action/kind/input/output/error and the rest to attributes", () => {
|
|
142
|
+
const { events } = normalizeMemoryEvents([
|
|
143
|
+
memoryEvent({
|
|
144
|
+
payload: [
|
|
145
|
+
{
|
|
146
|
+
blob: {
|
|
147
|
+
action: "Transfer",
|
|
148
|
+
kind: "request",
|
|
149
|
+
input: { amount: 10 },
|
|
150
|
+
output: { ok: true },
|
|
151
|
+
toolVersion: "3",
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
}),
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
expect(events).toEqual([
|
|
159
|
+
{
|
|
160
|
+
timeMs: 1_700_000_000_000,
|
|
161
|
+
sessionId: "sess-1",
|
|
162
|
+
eventId: "evt-1",
|
|
163
|
+
actor: "alice",
|
|
164
|
+
target: "mem-1",
|
|
165
|
+
action: "Transfer",
|
|
166
|
+
kind: "request",
|
|
167
|
+
input: { amount: 10 },
|
|
168
|
+
output: { ok: true },
|
|
169
|
+
attributes: { toolVersion: "3" },
|
|
170
|
+
},
|
|
171
|
+
]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("a conversational payload maps the role to an action and kind", () => {
|
|
175
|
+
const { events } = normalizeMemoryEvents([
|
|
176
|
+
memoryEvent({ payload: [{ conversational: { role: "TOOL", content: { text: "called" } } }] }),
|
|
177
|
+
]);
|
|
178
|
+
expect(events[0]).toMatchObject({
|
|
179
|
+
action: "InvokeTool",
|
|
180
|
+
kind: "request",
|
|
181
|
+
input: { text: "called" },
|
|
182
|
+
attributes: { role: "TOOL" },
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("an unknown role falls back to OTHER, and the role is preserved in attributes", () => {
|
|
187
|
+
const { events } = normalizeMemoryEvents([
|
|
188
|
+
memoryEvent({ payload: [{ conversational: { role: "SYSTEM", content: { text: "x" } } }] }),
|
|
189
|
+
]);
|
|
190
|
+
expect(events[0]).toMatchObject({ action: "Event", kind: "request", attributes: { role: "SYSTEM" } });
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("the role mapping is overridable", () => {
|
|
194
|
+
const { events } = normalizeMemoryEvents(
|
|
195
|
+
[memoryEvent({ payload: [{ conversational: { role: "USER", content: { text: "hi" } } }] })],
|
|
196
|
+
{ roles: { USER: { action: "Ask", kind: "prompt" } } },
|
|
197
|
+
);
|
|
198
|
+
expect(events[0]).toMatchObject({ action: "Ask", kind: "prompt" });
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("a multi-member payload becomes one event per member, with distinct ids", () => {
|
|
202
|
+
const { events } = normalizeMemoryEvents([
|
|
203
|
+
memoryEvent({
|
|
204
|
+
payload: [
|
|
205
|
+
{ blob: { action: "Transfer", input: { amount: 1 } } },
|
|
206
|
+
{ conversational: { role: "ASSISTANT", content: { text: "done" } } },
|
|
207
|
+
],
|
|
208
|
+
}),
|
|
209
|
+
]);
|
|
210
|
+
expect(events.map((e) => e.eventId)).toEqual(["evt-1#0", "evt-1#1"]);
|
|
211
|
+
expect(events.map((e) => e.timeMs)).toEqual([1_700_000_000_000, 1_700_000_000_000]);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("a non-record blob group is carried under `value`, not dropped", () => {
|
|
215
|
+
// A blob is arbitrary JSON; `input: "the prompt"` is ordinary.
|
|
216
|
+
const { events } = normalizeMemoryEvents([
|
|
217
|
+
memoryEvent({
|
|
218
|
+
payload: [{ blob: { action: "Ask", kind: "request", input: "prompt text", output: ["a", "b"] } }],
|
|
219
|
+
}),
|
|
220
|
+
]);
|
|
221
|
+
expect(events[0]).toMatchObject({ input: { value: "prompt text" }, output: { value: ["a", "b"] } });
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("target defaults to the memoryId and is overridable", () => {
|
|
225
|
+
expect(normalizeMemoryEvents([memoryEvent()]).events[0]?.target).toBe("mem-1");
|
|
226
|
+
expect(normalizeMemoryEvents([memoryEvent()], { target: "gw-1" }).events[0]?.target).toBe("gw-1");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("renames from nested payloads are carried out", () => {
|
|
230
|
+
const { renames } = normalizeMemoryEvents([
|
|
231
|
+
memoryEvent({ payload: [{ blob: { action: "T", input: { "user-id": "u1" } } }] }),
|
|
232
|
+
]);
|
|
233
|
+
expect(renames).toEqual([
|
|
234
|
+
{ path: "event 0 (evt-1).payload[0].blob.input.user-id", from: "user-id", to: "user_id" },
|
|
235
|
+
]);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe("a malformed history fails loudly", () => {
|
|
240
|
+
test("an empty payload throws rather than producing a payload-less trace", () => {
|
|
241
|
+
expect(() => normalizeMemoryEvents([memoryEvent({ payload: [] })])).toThrow(
|
|
242
|
+
/has an empty payload/,
|
|
243
|
+
);
|
|
244
|
+
expect(() => normalizeMemoryEvents([memoryEvent({ payload: undefined })])).toThrow(
|
|
245
|
+
AgentCoreTraceError,
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("a payload member that is neither conversational nor blob throws", () => {
|
|
250
|
+
expect(() =>
|
|
251
|
+
normalizeMemoryEvents([memoryEvent({ payload: [{} as never] })]),
|
|
252
|
+
).toThrow(/neither conversational nor blob/);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("…and says why skipping it would be worse", () => {
|
|
256
|
+
expect(() => normalizeMemoryEvents([memoryEvent({ payload: [{} as never] })])).toThrow(
|
|
257
|
+
/drop a decision point from the replay/,
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("a missing timestamp names the offending event", () => {
|
|
262
|
+
expect(() => normalizeMemoryEvents([memoryEvent({ eventTimestamp: undefined })])).toThrow(
|
|
263
|
+
/event 0 \(evt-1\)'s eventTimestamp/,
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("an event with no actorId is refused by the renderer, not smuggled through", async () => {
|
|
268
|
+
const { http } = mockHttp([{ body: { events: [memoryEvent({ actorId: undefined })] } }]);
|
|
269
|
+
await expect(awsAgentCoreFetchTrace(BASE, undefined, http)).rejects.toThrow(/has no actor/);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe("listMemoryEvents — transport", () => {
|
|
274
|
+
test("POSTs the documented Memory route with includePayloads", async () => {
|
|
275
|
+
const { http, calls } = mockHttp([{ body: { events: [memoryEvent()] } }]);
|
|
276
|
+
await listMemoryEvents(BASE, undefined, http);
|
|
277
|
+
|
|
278
|
+
expect(calls).toHaveLength(1);
|
|
279
|
+
expect(calls[0]?.url).toBe("http://localhost:4566/memories/mem-1/actor/alice/sessions/sess-1");
|
|
280
|
+
expect(calls[0]?.body).toMatchObject({ includePayloads: true, maxResults: 100 });
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("the endpoint override is what retargets it; without one it is the regional host", async () => {
|
|
284
|
+
const { http, calls } = mockHttp([{ body: { events: [] } }]);
|
|
285
|
+
await listMemoryEvents({ ...BASE, endpoint: undefined, region: "eu-west-1" }, undefined, http);
|
|
286
|
+
expect(calls[0]?.url).toBe(
|
|
287
|
+
"https://bedrock-agentcore.eu-west-1.amazonaws.com/memories/mem-1/actor/alice/sessions/sess-1",
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("path segments are encoded", async () => {
|
|
292
|
+
const { http, calls } = mockHttp([{ body: { events: [] } }]);
|
|
293
|
+
await listMemoryEvents({ ...BASE, sessionId: "a/b c" }, undefined, http);
|
|
294
|
+
expect(calls[0]?.url).toBe("http://localhost:4566/memories/mem-1/actor/alice/sessions/a%2Fb%20c");
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("pagination follows nextToken to exhaustion", async () => {
|
|
298
|
+
const { http, calls } = mockHttp([
|
|
299
|
+
{ body: { events: [memoryEvent({ eventId: "a" })], nextToken: "t1" } },
|
|
300
|
+
{ body: { events: [memoryEvent({ eventId: "b" })] } },
|
|
301
|
+
]);
|
|
302
|
+
const events = await listMemoryEvents(BASE, undefined, http);
|
|
303
|
+
|
|
304
|
+
expect(events.map((e) => e.eventId)).toEqual(["a", "b"]);
|
|
305
|
+
expect(calls[1]?.body).toMatchObject({ nextToken: "t1" });
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("maxEvents caps the fetch and shrinks the last page request", async () => {
|
|
309
|
+
const { http, calls } = mockHttp([
|
|
310
|
+
{ body: { events: [memoryEvent({ eventId: "a" }), memoryEvent({ eventId: "b" })], nextToken: "t1" } },
|
|
311
|
+
{ body: { events: [memoryEvent({ eventId: "c" })] } },
|
|
312
|
+
]);
|
|
313
|
+
const events = await listMemoryEvents({ ...BASE, maxEvents: 3 }, undefined, http);
|
|
314
|
+
|
|
315
|
+
expect(events).toHaveLength(3);
|
|
316
|
+
expect(calls[0]?.body.maxResults).toBe(3);
|
|
317
|
+
expect(calls[1]?.body.maxResults).toBe(1);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("an empty page ends the walk even when a token comes back", async () => {
|
|
321
|
+
const { http, calls } = mockHttp([{ body: { events: [], nextToken: "forever" } }]);
|
|
322
|
+
expect(await listMemoryEvents(BASE, undefined, http)).toEqual([]);
|
|
323
|
+
expect(calls).toHaveLength(1);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("a service error becomes a typed AwsReadError carrying the __type", async () => {
|
|
327
|
+
const { http } = mockHttp([
|
|
328
|
+
{ status: 404, body: { __type: "com.amazon#ResourceNotFoundException", message: "no such memory" } },
|
|
329
|
+
]);
|
|
330
|
+
await expect(listMemoryEvents(BASE, undefined, http)).rejects.toMatchObject({
|
|
331
|
+
name: "AwsReadError",
|
|
332
|
+
code: "ResourceNotFoundException",
|
|
333
|
+
message: "no such memory",
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("an unparseable body is a failed read, not an empty session", async () => {
|
|
338
|
+
const http: AwsReadHttp = async () => ({ status: 200, text: "<html>gateway timeout</html>" });
|
|
339
|
+
await expect(listMemoryEvents(BASE, undefined, http)).rejects.toBeInstanceOf(AwsReadError);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("a real-AWS read is signed with SigV4 over bedrock-agentcore (#1686)", async () => {
|
|
343
|
+
const { http, calls } = mockHttp([{ body: { events: [] } }]);
|
|
344
|
+
await listMemoryEvents(
|
|
345
|
+
{
|
|
346
|
+
...BASE,
|
|
347
|
+
endpoint: undefined,
|
|
348
|
+
region: "eu-west-1",
|
|
349
|
+
credentials: { accessKeyId: "AKID", secretAccessKey: "secret" },
|
|
350
|
+
now: new Date("2026-08-10T00:00:00Z"),
|
|
351
|
+
},
|
|
352
|
+
undefined,
|
|
353
|
+
http,
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
const auth = calls[0]?.headers.authorization ?? "";
|
|
357
|
+
expect(auth).toContain("Credential=AKID/20260810/eu-west-1/bedrock-agentcore/aws4_request");
|
|
358
|
+
expect(auth).not.toContain("Signature=unsigned");
|
|
359
|
+
// The body is what was signed, so a payload hash has to be on the wire.
|
|
360
|
+
expect(calls[0]?.headers["x-amz-content-sha256"]).toMatch(/^[0-9a-f]{64}$/);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test("an endpoint override carries the scope and no signature — an emulator verifies neither", async () => {
|
|
364
|
+
const { http, calls } = mockHttp([{ body: { events: [] } }]);
|
|
365
|
+
await listMemoryEvents(
|
|
366
|
+
{ ...BASE, region: "us-west-2", credentials: { accessKeyId: "AKID", secretAccessKey: "secret" } },
|
|
367
|
+
undefined,
|
|
368
|
+
http,
|
|
369
|
+
);
|
|
370
|
+
expect(calls[0]?.headers.authorization).toContain("Signature=unsigned");
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("signEndpointOverride signs anyway, for an override that is real AWS", async () => {
|
|
374
|
+
const { http, calls } = mockHttp([{ body: { events: [] } }]);
|
|
375
|
+
await listMemoryEvents(
|
|
376
|
+
{
|
|
377
|
+
...BASE,
|
|
378
|
+
region: "us-west-2",
|
|
379
|
+
credentials: { accessKeyId: "AKID", secretAccessKey: "secret" },
|
|
380
|
+
signEndpointOverride: true,
|
|
381
|
+
},
|
|
382
|
+
undefined,
|
|
383
|
+
http,
|
|
384
|
+
);
|
|
385
|
+
expect(calls[0]?.headers.authorization).not.toContain("Signature=unsigned");
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test("no credentials, no signature — the emulator path needs none", async () => {
|
|
389
|
+
const { http, calls } = mockHttp([{ body: { events: [] } }]);
|
|
390
|
+
await listMemoryEvents(
|
|
391
|
+
{ ...BASE, endpoint: undefined, region: "us-east-1", credentials: () => undefined, env: {} },
|
|
392
|
+
undefined,
|
|
393
|
+
http,
|
|
394
|
+
);
|
|
395
|
+
expect(calls[0]?.headers.authorization).toContain("Signature=unsigned");
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("maxEvents below 1 is refused, since ListEvents bounds maxResults at 1..100", async () => {
|
|
399
|
+
const { http, calls } = mockHttp([{ body: { events: [] } }]);
|
|
400
|
+
await expect(listMemoryEvents({ ...BASE, maxEvents: 0 }, undefined, http)).rejects.toThrow(
|
|
401
|
+
/maxEvents must be a positive integer/,
|
|
402
|
+
);
|
|
403
|
+
expect(calls).toHaveLength(0);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test("a missing identifier is refused before any request goes out", async () => {
|
|
407
|
+
const { http, calls } = mockHttp([{ body: { events: [] } }]);
|
|
408
|
+
await expect(listMemoryEvents({ ...BASE, memoryId: "" }, undefined, http)).rejects.toThrow(
|
|
409
|
+
/memoryId is required/,
|
|
410
|
+
);
|
|
411
|
+
expect(calls).toHaveLength(0);
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
describe("awsAgentCoreFetchTrace — fetch to trace text", () => {
|
|
416
|
+
test("renders the fetched history with both bags populated and the action qualified", async () => {
|
|
417
|
+
const { http } = mockHttp([
|
|
418
|
+
{
|
|
419
|
+
body: {
|
|
420
|
+
events: [
|
|
421
|
+
memoryEvent({
|
|
422
|
+
eventId: "u1",
|
|
423
|
+
eventTimestamp: 1_700_000_000,
|
|
424
|
+
payload: [{ blob: { action: "Login", kind: "request", input: { user: "alice" } } }],
|
|
425
|
+
}),
|
|
426
|
+
],
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
]);
|
|
430
|
+
|
|
431
|
+
const result = await awsAgentCoreFetchTrace(
|
|
432
|
+
{ ...BASE, namespace: "Drupe", principalType: "OAuthUser", resourceType: "Gateway", origin: "relative-seconds" },
|
|
433
|
+
undefined,
|
|
434
|
+
http,
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
expect(result.text).toBe(
|
|
438
|
+
'@0 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"mem-1") ' +
|
|
439
|
+
'request_context(input: { user: "alice" }) ' +
|
|
440
|
+
'Drupe::Action::"Login"::request(input: { user: "alice" }, ' +
|
|
441
|
+
'callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"mem-1", ' +
|
|
442
|
+
'sessionId: "sess-1", requestId: "u1")\n',
|
|
443
|
+
);
|
|
444
|
+
expect(result).toMatchObject({ source: "memory", sessionId: "sess-1", lineCount: 1, fetched: 1 });
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test("the window is applied after the fetch, because ListEvents has no time filter", async () => {
|
|
448
|
+
const { http, calls } = mockHttp([
|
|
449
|
+
{
|
|
450
|
+
body: {
|
|
451
|
+
events: [
|
|
452
|
+
memoryEvent({ eventId: "old", eventTimestamp: 1_000 }),
|
|
453
|
+
memoryEvent({ eventId: "keep", eventTimestamp: 2_000 }),
|
|
454
|
+
memoryEvent({ eventId: "new", eventTimestamp: 3_000 }),
|
|
455
|
+
],
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
]);
|
|
459
|
+
|
|
460
|
+
// The bounds go through the same reader the event timestamps do, so a
|
|
461
|
+
// bound is never on a different scale from what it is compared against.
|
|
462
|
+
const result = await awsAgentCoreFetchTrace({ ...BASE, since: 2_000, until: 2_000 }, undefined, http);
|
|
463
|
+
|
|
464
|
+
expect(calls[0]?.body).not.toHaveProperty("filter");
|
|
465
|
+
expect(result.fetched).toBe(3);
|
|
466
|
+
expect(result.lineCount).toBe(1);
|
|
467
|
+
expect(result.text).toContain('requestId: "keep"');
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
test("an event outside the window cannot fail the run", async () => {
|
|
471
|
+
// The window is applied before normalizing, so a payload-less record the
|
|
472
|
+
// caller deliberately excluded is not a reason to abort.
|
|
473
|
+
const { http } = mockHttp([
|
|
474
|
+
{
|
|
475
|
+
body: {
|
|
476
|
+
events: [
|
|
477
|
+
memoryEvent({ eventId: "broken", eventTimestamp: 1_000, payload: [] }),
|
|
478
|
+
memoryEvent({ eventId: "keep", eventTimestamp: 2_000 }),
|
|
479
|
+
],
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
]);
|
|
483
|
+
const result = await awsAgentCoreFetchTrace({ ...BASE, since: 2_000 }, undefined, http);
|
|
484
|
+
expect(result.lineCount).toBe(1);
|
|
485
|
+
expect(result.text).toContain('requestId: "keep"');
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
test("…but one inside the window still fails the run", async () => {
|
|
489
|
+
const { http } = mockHttp([
|
|
490
|
+
{ body: { events: [memoryEvent({ eventId: "broken", eventTimestamp: 2_000, payload: [] })] } },
|
|
491
|
+
]);
|
|
492
|
+
await expect(awsAgentCoreFetchTrace({ ...BASE, since: 2_000 }, undefined, http)).rejects.toThrow(
|
|
493
|
+
/has an empty payload/,
|
|
494
|
+
);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
test("an ISO window bound is accepted", async () => {
|
|
498
|
+
const { http } = mockHttp([
|
|
499
|
+
{ body: { events: [memoryEvent({ eventTimestamp: "2023-11-14T22:13:20.000Z" })] } },
|
|
500
|
+
]);
|
|
501
|
+
const result = await awsAgentCoreFetchTrace(
|
|
502
|
+
{ ...BASE, since: "2023-11-14T00:00:00.000Z", until: "2023-11-15T00:00:00.000Z" },
|
|
503
|
+
undefined,
|
|
504
|
+
http,
|
|
505
|
+
);
|
|
506
|
+
expect(result.lineCount).toBe(1);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
test("an empty window throws, naming the Memory caveat rather than shrugging", async () => {
|
|
510
|
+
const { http } = mockHttp([{ body: { events: [memoryEvent({ eventTimestamp: 1_000 })] } }]);
|
|
511
|
+
await expect(
|
|
512
|
+
awsAgentCoreFetchTrace({ ...BASE, since: 9_000_000_000_000 }, undefined, http),
|
|
513
|
+
).rejects.toThrow(/holds what the agent wrote through CreateEvent/);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test("requireEvents: false accepts an empty trace", async () => {
|
|
517
|
+
const { http } = mockHttp([{ body: { events: [] } }]);
|
|
518
|
+
const result = await awsAgentCoreFetchTrace({ ...BASE, requireEvents: false }, undefined, http);
|
|
519
|
+
expect(result).toMatchObject({ text: "", lineCount: 0, fetched: 0 });
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test("outPath writes the trace PolicyReplayOp reads, and reports the absolute path", async () => {
|
|
523
|
+
const { http } = mockHttp([{ body: { events: [memoryEvent()] } }]);
|
|
524
|
+
const dir = await mkdtemp(join(tmpdir(), "chant-agentcore-"));
|
|
525
|
+
const outPath = join(dir, "nested", "session.log");
|
|
526
|
+
|
|
527
|
+
const result = await awsAgentCoreFetchTrace({ ...BASE, outPath }, undefined, http);
|
|
528
|
+
|
|
529
|
+
expect(result.outPath).toBe(outPath);
|
|
530
|
+
expect(await readFile(outPath, "utf8")).toBe(result.text);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test("renames are surfaced on the result rather than happening silently", async () => {
|
|
534
|
+
const { http } = mockHttp([
|
|
535
|
+
{ body: { events: [memoryEvent({ payload: [{ blob: { action: "T", input: { "user-id": "u1" } } }] })] } },
|
|
536
|
+
]);
|
|
537
|
+
const result = await awsAgentCoreFetchTrace(BASE, undefined, http);
|
|
538
|
+
expect(result.renames).toHaveLength(1);
|
|
539
|
+
expect(result.text).toContain('input: { user_id: "u1" }');
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
test("a decision-kind event with no payload is refused, not rendered weak", async () => {
|
|
543
|
+
const { http } = mockHttp([{ body: { events: [memoryEvent({ payload: [{ blob: { action: "Ping" } }] })] } }]);
|
|
544
|
+
await expect(awsAgentCoreFetchTrace(BASE, undefined, http)).rejects.toThrow(
|
|
545
|
+
/request_context envelope would be empty/,
|
|
546
|
+
);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
test("…and `allow` is the way to say the weakening is the point", async () => {
|
|
550
|
+
const { http } = mockHttp([{ body: { events: [memoryEvent({ payload: [{ blob: { action: "Ping" } }] })] } }]);
|
|
551
|
+
const result = await awsAgentCoreFetchTrace(
|
|
552
|
+
{ ...BASE, allow: ["no-request-context"] },
|
|
553
|
+
undefined,
|
|
554
|
+
http,
|
|
555
|
+
);
|
|
556
|
+
expect(result.issues.map((i) => i.kind)).toEqual(["no-request-context"]);
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
describe("sources this module refuses rather than guesses at (#1685 investigation)", () => {
|
|
561
|
+
test("gateway-logs names the CloudWatch finding and the non-JSON requestBody", async () => {
|
|
562
|
+
const { http, calls } = mockHttp([{ body: { events: [] } }]);
|
|
563
|
+
const promise = awsAgentCoreFetchTrace({ ...BASE, source: "gateway-logs" }, undefined, http);
|
|
564
|
+
|
|
565
|
+
await expect(promise).rejects.toBeInstanceOf(AgentCoreTraceUnavailableError);
|
|
566
|
+
await expect(promise).rejects.toThrow(/vendedlogs\/bedrock-agentcore\/gateway/);
|
|
567
|
+
await expect(promise).rejects.toThrow(/Java map toString/);
|
|
568
|
+
expect(calls).toHaveLength(0);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
test("spans names the policy-decision span attribute and the Transaction Search requirement", async () => {
|
|
572
|
+
const { http } = mockHttp([{ body: { events: [] } }]);
|
|
573
|
+
const promise = awsAgentCoreFetchTrace({ ...BASE, source: "spans" }, undefined, http);
|
|
574
|
+
|
|
575
|
+
await expect(promise).rejects.toThrow(/aws\.agentcore\.policy\.authorization_decision/);
|
|
576
|
+
await expect(promise).rejects.toThrow(/Transaction\s+Search/);
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
test("the error carries the source, so a caller can branch on it", async () => {
|
|
580
|
+
const { http } = mockHttp([{ body: { events: [] } }]);
|
|
581
|
+
try {
|
|
582
|
+
await awsAgentCoreFetchTrace({ ...BASE, source: "spans" }, undefined, http);
|
|
583
|
+
expect.unreachable("expected a refusal");
|
|
584
|
+
} catch (error) {
|
|
585
|
+
expect((error as AgentCoreTraceUnavailableError).source).toBe("spans");
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
});
|