@intx/inference 0.1.2 → 0.2.2

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.
Files changed (97) hide show
  1. package/LICENSE +176 -0
  2. package/dist/actions.d.ts +16 -0
  3. package/dist/actions.js +200 -0
  4. package/dist/adapter.d.ts +38 -0
  5. package/dist/adapter.js +31 -0
  6. package/dist/assembly.d.ts +68 -0
  7. package/dist/assembly.js +132 -0
  8. package/dist/audit-collector.d.ts +10 -0
  9. package/dist/audit-collector.js +139 -0
  10. package/dist/auth.d.ts +24 -0
  11. package/{src/auth.ts → dist/auth.js} +13 -19
  12. package/dist/authz-extension.d.ts +32 -0
  13. package/dist/authz-extension.js +100 -0
  14. package/dist/correlation.d.ts +25 -0
  15. package/dist/correlation.js +32 -0
  16. package/dist/default-director.d.ts +111 -0
  17. package/dist/default-director.js +199 -0
  18. package/dist/director.d.ts +6 -0
  19. package/dist/director.js +56 -0
  20. package/dist/errors.d.ts +18 -0
  21. package/dist/errors.js +83 -0
  22. package/dist/gates.d.ts +27 -0
  23. package/dist/gates.js +80 -0
  24. package/dist/harness.d.ts +147 -0
  25. package/dist/harness.js +1319 -0
  26. package/dist/index.d.ts +37 -0
  27. package/dist/index.js +21 -0
  28. package/dist/manifest.d.ts +31 -0
  29. package/dist/manifest.js +44 -0
  30. package/dist/providers/anthropic.d.ts +33 -0
  31. package/dist/providers/anthropic.js +670 -0
  32. package/dist/providers/google-genai-files.d.ts +48 -0
  33. package/dist/providers/google-genai-files.js +205 -0
  34. package/dist/providers/google-genai.d.ts +3 -0
  35. package/dist/providers/google-genai.js +1196 -0
  36. package/dist/providers/index.d.ts +38 -0
  37. package/dist/providers/index.js +56 -0
  38. package/dist/providers/openai.d.ts +3 -0
  39. package/dist/providers/openai.js +609 -0
  40. package/dist/reactor.d.ts +50 -0
  41. package/dist/reactor.js +920 -0
  42. package/dist/retry-policy.d.ts +31 -0
  43. package/{src/retry-policy.ts → dist/retry-policy.js} +41 -53
  44. package/dist/sse.d.ts +1 -0
  45. package/dist/sse.js +63 -0
  46. package/dist/state.d.ts +23 -0
  47. package/dist/state.js +100 -0
  48. package/dist/tool-name.d.ts +6 -0
  49. package/dist/tool-name.js +110 -0
  50. package/dist/transform.d.ts +11 -0
  51. package/dist/transform.js +117 -0
  52. package/dist/transforms/index.d.ts +2 -0
  53. package/dist/transforms/index.js +1 -0
  54. package/dist/transforms/size-cap.d.ts +12 -0
  55. package/dist/transforms/size-cap.js +80 -0
  56. package/dist/turns.d.ts +21 -0
  57. package/dist/turns.js +135 -0
  58. package/package.json +21 -6
  59. package/src/actions.ts +0 -245
  60. package/src/adapter.ts +0 -57
  61. package/src/assembly.test.ts +0 -728
  62. package/src/assembly.ts +0 -250
  63. package/src/audit-collector.test.ts +0 -332
  64. package/src/audit-collector.ts +0 -172
  65. package/src/auth.test.ts +0 -117
  66. package/src/authz-extension.test.ts +0 -269
  67. package/src/authz-extension.ts +0 -145
  68. package/src/correlation.ts +0 -61
  69. package/src/default-director.test.ts +0 -314
  70. package/src/default-director.ts +0 -344
  71. package/src/director.ts +0 -87
  72. package/src/errors.test.ts +0 -133
  73. package/src/errors.ts +0 -115
  74. package/src/gates.ts +0 -128
  75. package/src/harness.test.ts +0 -655
  76. package/src/harness.ts +0 -1571
  77. package/src/index.ts +0 -76
  78. package/src/providers/anthropic.test.ts +0 -771
  79. package/src/providers/anthropic.ts +0 -810
  80. package/src/providers/google-genai-files.ts +0 -289
  81. package/src/providers/google-genai.ts +0 -1518
  82. package/src/providers/openai.ts +0 -719
  83. package/src/providers/registry.ts +0 -33
  84. package/src/reactor.test.ts +0 -3660
  85. package/src/reactor.ts +0 -1058
  86. package/src/scheduler.test.ts +0 -41
  87. package/src/sse.test.ts +0 -133
  88. package/src/sse.ts +0 -76
  89. package/src/state.ts +0 -135
  90. package/src/transform.test.ts +0 -207
  91. package/src/transform.ts +0 -159
  92. package/src/transforms/index.ts +0 -2
  93. package/src/transforms/size-cap.test.ts +0 -172
  94. package/src/transforms/size-cap.ts +0 -110
  95. package/src/turns.ts +0 -54
  96. package/tsconfig.json +0 -4
  97. package/tsconfig.tsbuildinfo +0 -1
@@ -1,41 +0,0 @@
1
- // Tests for the `Scheduler` time source. The harness reads
2
- // `scheduler.now()` to time deltas across multiple operations within a
3
- // single call; the contract that matters is monotonicity and the same
4
- // time domain as `setTimeout`. We do not assert specific values — the
5
- // production default uses `performance.now()` which is sub-ms and
6
- // floating-point.
7
-
8
- import { describe, test, expect } from "bun:test";
9
-
10
- import { createDefaultScheduler } from "./harness";
11
-
12
- describe("createDefaultScheduler", () => {
13
- test("now() returns a finite number", () => {
14
- const scheduler = createDefaultScheduler();
15
- const value = scheduler.now();
16
- expect(Number.isFinite(value)).toBe(true);
17
- });
18
-
19
- test("now() is monotonic: a second read is not less than the first", () => {
20
- const scheduler = createDefaultScheduler();
21
- const a = scheduler.now();
22
- const b = scheduler.now();
23
- expect(b).toBeGreaterThanOrEqual(a);
24
- });
25
-
26
- test("now() advances across a setTimeout that fires", async () => {
27
- const scheduler = createDefaultScheduler();
28
- const before = scheduler.now();
29
- await new Promise<void>((resolve) => {
30
- scheduler.setTimeout(() => {
31
- resolve();
32
- }, 5);
33
- });
34
- const after = scheduler.now();
35
- // Real wall-clock time elapsed across the await must be reflected
36
- // in `now()` — the time domain of `setTimeout` and `now()` is the
37
- // contract that lets the retry wrapper compute `elapsedMs` from
38
- // two `now()` reads sandwiching a `setTimeout`-driven delay.
39
- expect(after - before).toBeGreaterThanOrEqual(5);
40
- });
41
- });
package/src/sse.test.ts DELETED
@@ -1,133 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
- import { parseSSE } from "./sse";
3
-
4
- function makeStream(chunks: string[]): ReadableStream<Uint8Array> {
5
- const encoder = new TextEncoder();
6
- return new ReadableStream({
7
- start(controller) {
8
- for (const chunk of chunks) {
9
- controller.enqueue(encoder.encode(chunk));
10
- }
11
- controller.close();
12
- },
13
- });
14
- }
15
-
16
- async function collectSSE(
17
- stream: ReadableStream<Uint8Array>,
18
- ): Promise<string[]> {
19
- const results: string[] = [];
20
- for await (const data of parseSSE(stream)) {
21
- results.push(data);
22
- }
23
- return results;
24
- }
25
-
26
- describe("parseSSE", () => {
27
- test("parses a single complete data line", async () => {
28
- const stream = makeStream(["data: hello\n\n"]);
29
- const results = await collectSSE(stream);
30
- expect(results).toEqual(["hello"]);
31
- });
32
-
33
- test("parses multiple data events", async () => {
34
- const stream = makeStream(["data: first\n\ndata: second\n\n"]);
35
- const results = await collectSSE(stream);
36
- expect(results).toEqual(["first", "second"]);
37
- });
38
-
39
- test("strips the optional space after data:", async () => {
40
- const stream = makeStream(["data:nospace\n\ndata: withspace\n\n"]);
41
- const results = await collectSSE(stream);
42
- expect(results).toEqual(["nospace", "withspace"]);
43
- });
44
-
45
- test("skips comment lines", async () => {
46
- const stream = makeStream([": this is a comment\ndata: payload\n\n"]);
47
- const results = await collectSSE(stream);
48
- expect(results).toEqual(["payload"]);
49
- });
50
-
51
- test("skips blank lines between events", async () => {
52
- const stream = makeStream(["data: a\n\n\ndata: b\n\n"]);
53
- const results = await collectSSE(stream);
54
- expect(results).toEqual(["a", "b"]);
55
- });
56
-
57
- test("stops at [DONE] sentinel", async () => {
58
- const stream = makeStream(["data: a\n\ndata: [DONE]\n\ndata: b\n\n"]);
59
- const results = await collectSSE(stream);
60
- expect(results).toEqual(["a"]);
61
- });
62
-
63
- test("handles CRLF line endings", async () => {
64
- const stream = makeStream(["data: hello\r\n\r\n"]);
65
- const results = await collectSSE(stream);
66
- expect(results).toEqual(["hello"]);
67
- });
68
-
69
- test("handles data split across chunks", async () => {
70
- // The data line is split mid-word across two chunks.
71
- const stream = makeStream(["data: hel", "lo\n\n"]);
72
- const results = await collectSSE(stream);
73
- expect(results).toEqual(["hello"]);
74
- });
75
-
76
- test("handles newline split across chunks", async () => {
77
- const stream = makeStream(["data: hello\n", "\n"]);
78
- const results = await collectSSE(stream);
79
- expect(results).toEqual(["hello"]);
80
- });
81
-
82
- test("handles many small chunks", async () => {
83
- const stream = makeStream([
84
- "d",
85
- "a",
86
- "t",
87
- "a",
88
- ":",
89
- " ",
90
- "t",
91
- "o",
92
- "k",
93
- "e",
94
- "n",
95
- "\n",
96
- "\n",
97
- ]);
98
- const results = await collectSSE(stream);
99
- expect(results).toEqual(["token"]);
100
- });
101
-
102
- test("ignores non-data field lines", async () => {
103
- const stream = makeStream([
104
- "event: content_block_delta\ndata: payload\n\n",
105
- ]);
106
- const results = await collectSSE(stream);
107
- expect(results).toEqual(["payload"]);
108
- });
109
-
110
- test("handles empty stream", async () => {
111
- const stream = makeStream([]);
112
- const results = await collectSSE(stream);
113
- expect(results).toEqual([]);
114
- });
115
-
116
- test("handles stream with only comments", async () => {
117
- const stream = makeStream([": ping\n: ping\n"]);
118
- const results = await collectSSE(stream);
119
- expect(results).toEqual([]);
120
- });
121
-
122
- test("parses JSON data payloads", async () => {
123
- const stream = makeStream(['data: {"type":"text","text":"hello"}\n\n']);
124
- const results = await collectSSE(stream);
125
- expect(results).toEqual(['{"type":"text","text":"hello"}']);
126
- });
127
-
128
- test("handles multiple events in one chunk", async () => {
129
- const stream = makeStream(["data: one\n\ndata: two\n\ndata: three\n\n"]);
130
- const results = await collectSSE(stream);
131
- expect(results).toEqual(["one", "two", "three"]);
132
- });
133
- });
package/src/sse.ts DELETED
@@ -1,76 +0,0 @@
1
- // Server-Sent Events byte stream parser.
2
- //
3
- // Converts a ReadableStream<Uint8Array> (the raw HTTP response body) into an
4
- // AsyncIterable<string> of SSE data payloads. Each yielded string is the
5
- // value of one `data:` field. Comments (`:`) and blank-line separators are
6
- // consumed internally. The `[DONE]` sentinel (OpenAI convention) terminates
7
- // the iteration.
8
- //
9
- // The parser buffers incomplete lines across chunk boundaries so split chunks
10
- // are handled correctly regardless of where chunk boundaries fall.
11
-
12
- const decoder = new TextDecoder();
13
-
14
- export async function* parseSSE(
15
- stream: ReadableStream<Uint8Array>,
16
- ): AsyncIterable<string> {
17
- const reader = stream.getReader();
18
- let buffer = "";
19
-
20
- try {
21
- while (true) {
22
- const { done, value } = await reader.read();
23
-
24
- if (done) {
25
- // Flush any remaining content in the buffer as a final line.
26
- if (buffer.length > 0) {
27
- const payload = extractDataPayload(buffer);
28
- if (payload !== null) {
29
- yield payload;
30
- }
31
- }
32
- break;
33
- }
34
-
35
- buffer += decoder.decode(value, { stream: true });
36
-
37
- // Process all complete lines (lines terminated by \n).
38
- // A line ending in \r\n counts as terminated at the \n.
39
- let newlineIndex: number;
40
- while ((newlineIndex = buffer.indexOf("\n")) !== -1) {
41
- const rawLine = buffer.slice(0, newlineIndex);
42
- buffer = buffer.slice(newlineIndex + 1);
43
-
44
- // Strip trailing \r for CRLF line endings.
45
- const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
46
-
47
- // Blank lines and comment lines are ignored.
48
- if (line === "" || line.startsWith(":")) {
49
- continue;
50
- }
51
-
52
- const payload = extractDataPayload(line);
53
- if (payload === null) {
54
- continue;
55
- }
56
-
57
- if (payload === "[DONE]") {
58
- return;
59
- }
60
-
61
- yield payload;
62
- }
63
- }
64
- } finally {
65
- reader.releaseLock();
66
- }
67
- }
68
-
69
- function extractDataPayload(line: string): string | null {
70
- if (line.startsWith("data:")) {
71
- // The spec allows an optional space after the colon.
72
- const raw = line.slice(5);
73
- return raw.startsWith(" ") ? raw.slice(1) : raw;
74
- }
75
- return null;
76
- }
package/src/state.ts DELETED
@@ -1,135 +0,0 @@
1
- // Reactor state management: turn history, async operations, usage tracking.
2
- //
3
- // The state object is the authoritative view the director receives on every
4
- // decision. It is mutable by the reactor only — the director receives a
5
- // snapshot so it cannot corrupt the reactor's internal state.
6
- //
7
- // (INFERENCE.md § Agent Reactor › Director Decision Function)
8
-
9
- import type {
10
- ConversationTurn,
11
- LastCycleSource,
12
- PendingOperation,
13
- TokenUsage,
14
- ReactorState,
15
- } from "@intx/types/runtime";
16
- import type { GateSnapshot } from "./gates";
17
-
18
- export type ReactorStateManager = ReturnType<typeof createStateManager>;
19
-
20
- /**
21
- * Creates a mutable state container. All mutations go through explicit methods;
22
- * the `snapshot()` method produces an immutable view for the director.
23
- */
24
- export function createStateManager(
25
- sessionId: string,
26
- initialTurns: ConversationTurn[],
27
- initialOps: PendingOperation[],
28
- initialUsage: TokenUsage,
29
- ) {
30
- let turns: ConversationTurn[] = [...initialTurns];
31
- const pendingOperations = new Map<string, PendingOperation>(
32
- initialOps.map((op) => [op.correlationId, op]),
33
- );
34
- const tokenUsage: TokenUsage = { ...initialUsage };
35
- let lastCycleUsage: TokenUsage | null = null;
36
- let lastCycleSource: LastCycleSource | null = null;
37
- let activeGatesSnapshot: GateSnapshot[] = [];
38
- const activeForks: { forkId: string; mode: "independent" | "child" }[] = [];
39
-
40
- function appendTurn(msg: ConversationTurn): void {
41
- turns.push(msg);
42
- }
43
-
44
- function replaceTurns(next: ConversationTurn[]): void {
45
- turns = [...next];
46
- }
47
-
48
- function addPendingOperation(op: PendingOperation): void {
49
- pendingOperations.set(op.correlationId, op);
50
- }
51
-
52
- function removePendingOperation(correlationId: string): void {
53
- pendingOperations.delete(correlationId);
54
- }
55
-
56
- function accumUsage(usage: TokenUsage): void {
57
- tokenUsage.input += usage.input;
58
- tokenUsage.output += usage.output;
59
- tokenUsage.cacheRead += usage.cacheRead;
60
- tokenUsage.cacheWrite += usage.cacheWrite;
61
- tokenUsage.thinking += usage.thinking;
62
- }
63
-
64
- function setLastCycleUsage(usage: TokenUsage): void {
65
- lastCycleUsage = { ...usage };
66
- }
67
-
68
- function setLastCycleSource(source: LastCycleSource): void {
69
- lastCycleSource = { ...source };
70
- }
71
-
72
- function setGatesSnapshot(gates: GateSnapshot[]): void {
73
- activeGatesSnapshot = gates;
74
- }
75
-
76
- function addFork(forkId: string, mode: "independent" | "child"): void {
77
- activeForks.push({ forkId, mode });
78
- }
79
-
80
- function removeFork(forkId: string): void {
81
- const idx = activeForks.findIndex((f) => f.forkId === forkId);
82
- if (idx !== -1) activeForks.splice(idx, 1);
83
- }
84
-
85
- function getTurns(): ConversationTurn[] {
86
- return turns;
87
- }
88
-
89
- function getPendingOperations(): PendingOperation[] {
90
- return Array.from(pendingOperations.values());
91
- }
92
-
93
- function getTokenUsage(): TokenUsage {
94
- return { ...tokenUsage };
95
- }
96
-
97
- function snapshot(): ReactorState {
98
- return {
99
- sessionId,
100
- turns: turns.map((m) => ({
101
- ...m,
102
- content: m.content.map((b) => structuredClone(b)),
103
- })),
104
- pendingOperations: Array.from(pendingOperations.values()).map((op) =>
105
- structuredClone(op),
106
- ),
107
- activeGates: activeGatesSnapshot.map((g) => ({
108
- gateId: g.gateId,
109
- type: g.type,
110
- timeoutAt: g.timeoutAt,
111
- })),
112
- activeForks: activeForks.map((f) => ({ ...f })),
113
- tokenUsage: { ...tokenUsage },
114
- lastCycleUsage: lastCycleUsage !== null ? { ...lastCycleUsage } : null,
115
- lastCycleSource: lastCycleSource !== null ? { ...lastCycleSource } : null,
116
- };
117
- }
118
-
119
- return {
120
- appendTurn,
121
- replaceTurns,
122
- addPendingOperation,
123
- removePendingOperation,
124
- accumUsage,
125
- setLastCycleUsage,
126
- setLastCycleSource,
127
- setGatesSnapshot,
128
- addFork,
129
- removeFork,
130
- getTurns,
131
- getPendingOperations,
132
- getTokenUsage,
133
- snapshot,
134
- };
135
- }
@@ -1,207 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
- import { transformMessages, createIDNormalizer } from "./transform";
3
- import type { ConversationTurn } from "@intx/types/runtime";
4
-
5
- describe("transformMessages", () => {
6
- test("preserves messages when target model matches originating model", () => {
7
- const messages: ConversationTurn[] = [
8
- {
9
- role: "assistant",
10
- model: "claude-3-5-sonnet",
11
- content: [
12
- { type: "thinking", thinking: "Let me think..." },
13
- { type: "text", text: "Here is the answer." },
14
- ],
15
- timestamp: 1000,
16
- },
17
- ];
18
-
19
- const result = transformMessages(messages, {
20
- targetModel: "claude-3-5-sonnet",
21
- keepThinkingForSameModel: true,
22
- });
23
-
24
- expect(result).toHaveLength(1);
25
- const firstMsg = result[0];
26
- expect(firstMsg?.content).toHaveLength(2);
27
- expect(firstMsg?.content[0]?.type).toBe("thinking");
28
- });
29
-
30
- test("strips thinking blocks when replaying to a different model", () => {
31
- const messages: ConversationTurn[] = [
32
- {
33
- role: "assistant",
34
- model: "claude-3-5-sonnet",
35
- content: [
36
- { type: "thinking", thinking: "Some reasoning..." },
37
- { type: "text", text: "Answer." },
38
- ],
39
- timestamp: 1000,
40
- },
41
- ];
42
-
43
- const result = transformMessages(messages, {
44
- targetModel: "gpt-4o",
45
- });
46
-
47
- expect(result).toHaveLength(1);
48
- const firstMsg = result[0];
49
- expect(firstMsg?.content).toHaveLength(1);
50
- expect(firstMsg?.content[0]?.type).toBe("text");
51
- });
52
-
53
- test("strips thinking blocks when keepThinkingForSameModel is false", () => {
54
- const messages: ConversationTurn[] = [
55
- {
56
- role: "assistant",
57
- model: "claude-3-5-sonnet",
58
- content: [
59
- { type: "thinking", thinking: "Reasoning..." },
60
- { type: "text", text: "Answer." },
61
- ],
62
- timestamp: 1000,
63
- },
64
- ];
65
-
66
- const result = transformMessages(messages, {
67
- targetModel: "claude-3-5-sonnet",
68
- keepThinkingForSameModel: false,
69
- });
70
-
71
- const firstMsg = result[0];
72
- expect(firstMsg?.content).toHaveLength(1);
73
- expect(firstMsg?.content[0]?.type).toBe("text");
74
- });
75
-
76
- test("injects synthetic tool results for orphaned tool calls", () => {
77
- const messages: ConversationTurn[] = [
78
- {
79
- role: "user",
80
- content: [{ type: "text", text: "Do something." }],
81
- timestamp: 1000,
82
- },
83
- {
84
- role: "assistant",
85
- content: [
86
- {
87
- type: "tool_call",
88
- id: "call_1",
89
- name: "read_file",
90
- arguments: { path: "/tmp/foo" },
91
- },
92
- ],
93
- timestamp: 1000,
94
- },
95
- // No tool result follows — the conversation was interrupted.
96
- ];
97
-
98
- const result = transformMessages(messages, { targetModel: "gpt-4o" });
99
-
100
- // Should inject a synthetic tool result message.
101
- expect(result).toHaveLength(3);
102
- const injected = result[2];
103
- expect(injected?.role).toBe("user");
104
- expect(injected?.content[0]?.type).toBe("tool_result");
105
- const toolResult = injected?.content[0];
106
- if (toolResult?.type === "tool_result") {
107
- expect(toolResult.callId).toBe("call_1");
108
- expect(toolResult.isError).toBe(true);
109
- }
110
- });
111
-
112
- test("does not inject when tool results are present", () => {
113
- const messages: ConversationTurn[] = [
114
- {
115
- role: "user",
116
- content: [{ type: "text", text: "Do something." }],
117
- timestamp: 1000,
118
- },
119
- {
120
- role: "assistant",
121
- content: [
122
- {
123
- type: "tool_call",
124
- id: "call_1",
125
- name: "read_file",
126
- arguments: { path: "/tmp/foo" },
127
- },
128
- ],
129
- timestamp: 1000,
130
- },
131
- {
132
- role: "user",
133
- content: [
134
- {
135
- type: "tool_result",
136
- callId: "call_1",
137
- content: [{ type: "text", text: "file contents" }],
138
- },
139
- ],
140
- timestamp: 1000,
141
- },
142
- ];
143
-
144
- const result = transformMessages(messages, { targetModel: "gpt-4o" });
145
- expect(result).toHaveLength(3);
146
- });
147
-
148
- test("preserves user and system messages unchanged", () => {
149
- const messages: ConversationTurn[] = [
150
- {
151
- role: "system",
152
- content: [{ type: "text", text: "You are helpful." }],
153
- timestamp: 1000,
154
- },
155
- {
156
- role: "user",
157
- content: [{ type: "text", text: "Hello." }],
158
- timestamp: 1000,
159
- },
160
- ];
161
-
162
- const result = transformMessages(messages, { targetModel: "gpt-4o" });
163
- expect(result).toHaveLength(2);
164
- expect(result[0]).toEqual(messages[0]);
165
- expect(result[1]).toEqual(messages[1]);
166
- });
167
- });
168
-
169
- describe("createIDNormalizer", () => {
170
- test("assigns stable portable IDs", () => {
171
- const norm = createIDNormalizer();
172
- const id1 = norm.normalize(
173
- "call_abc_very_long_id_from_openai_responses_api",
174
- );
175
- const id2 = norm.normalize(
176
- "call_abc_very_long_id_from_openai_responses_api",
177
- );
178
- expect(id1).toBe(id2);
179
- expect(id1.startsWith("tc_")).toBe(true);
180
- });
181
-
182
- test("assigns different IDs to different provider IDs", () => {
183
- const norm = createIDNormalizer();
184
- const id1 = norm.normalize("toolu_01A");
185
- const id2 = norm.normalize("toolu_02B");
186
- expect(id1).not.toBe(id2);
187
- });
188
-
189
- test("resolves a portable ID back to the provider ID", () => {
190
- const norm = createIDNormalizer();
191
- const providerId = "toolu_01AbCdEfGhIjKlMnOpQrStUvWx";
192
- const portable = norm.normalize(providerId);
193
- expect(norm.resolve(portable)).toBe(providerId);
194
- });
195
-
196
- test("resolve returns undefined for unknown portable ID", () => {
197
- const norm = createIDNormalizer();
198
- expect(norm.resolve("tc_unknown")).toBeUndefined();
199
- });
200
-
201
- test("IDs are monotonically increasing", () => {
202
- const norm = createIDNormalizer();
203
- const ids = ["a", "b", "c"].map((x) => norm.normalize(x));
204
- // All different.
205
- expect(new Set(ids).size).toBe(3);
206
- });
207
- });