@alumnium/test 0.24.2 → 0.25.0-alpha.1
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/README.md +1 -1
- package/package.json +4 -3
- package/src/index.cjs +432 -28
- package/src/index.cjs.map +10 -5
- package/src/index.d.ts +4 -0
- package/src/index.js +435 -30
- package/src/index.js.map +10 -5
- package/src/masker/ToolCallMasker.d.ts +5 -1
- package/src/masker/alumniumMaskerProps.d.ts +16 -0
- package/src/player/BashExecutor.d.ts +16 -0
- package/src/player/McpExecutor.d.ts +37 -0
- package/src/player/Player.d.ts +53 -0
- package/src/player/ToolExecutor.d.ts +25 -0
package/README.md
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
# @alumnium/test
|
|
2
2
|
|
|
3
|
-
Alumnium test
|
|
3
|
+
Alumnium test runner. See [the main `alumnium` package page](https://www.npmjs.com/package/alumnium) for more details.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alumnium/test",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Alumnium test
|
|
3
|
+
"version": "0.25.0-alpha.1",
|
|
4
|
+
"description": "Alumnium test runner",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Alex Rodionov <p0deje@gmail.com>",
|
|
7
7
|
"contributors": [
|
|
@@ -32,8 +32,9 @@
|
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@alumnium/core": "0.
|
|
35
|
+
"@alumnium/core": "0.25.0-alpha.1",
|
|
36
36
|
"@js-fns/xxhash": "^0.1.0",
|
|
37
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
37
38
|
"zod": "^4.4.1"
|
|
38
39
|
}
|
|
39
40
|
}
|
package/src/index.cjs
CHANGED
|
@@ -68,11 +68,15 @@ var __export = (target, all) => {
|
|
|
68
68
|
// test/src/index.ts
|
|
69
69
|
var exports_src = {};
|
|
70
70
|
__export(exports_src, {
|
|
71
|
+
BashExecutor: () => BashExecutor,
|
|
71
72
|
FileTestCache: () => FileTestCache,
|
|
72
73
|
Masker: () => Masker,
|
|
74
|
+
McpExecutor: () => McpExecutor,
|
|
75
|
+
Player: () => Player,
|
|
73
76
|
Recorder: () => Recorder,
|
|
74
77
|
Recording: () => Recording,
|
|
75
78
|
RecordingLeakError: () => RecordingLeakError,
|
|
79
|
+
composeExecutors: () => composeExecutors,
|
|
76
80
|
});
|
|
77
81
|
module.exports = __toCommonJS(exports_src);
|
|
78
82
|
|
|
@@ -585,6 +589,12 @@ class ToolCallMasker {
|
|
|
585
589
|
return [];
|
|
586
590
|
}
|
|
587
591
|
const json = parseJsonStructure(text);
|
|
592
|
+
if (!json && options.secretsOnly) {
|
|
593
|
+
logger4.debug(
|
|
594
|
+
`Failed tool call ${callId} output is not JSON, ignoring it`,
|
|
595
|
+
);
|
|
596
|
+
return [];
|
|
597
|
+
}
|
|
588
598
|
if (!json) {
|
|
589
599
|
logger4.info(
|
|
590
600
|
`Tool call ${callId} output is not a JSON object or array, ignoring it for masking. ` +
|
|
@@ -805,8 +815,421 @@ class Masker {
|
|
|
805
815
|
);
|
|
806
816
|
}
|
|
807
817
|
}
|
|
808
|
-
// test/src/
|
|
818
|
+
// test/src/player/BashExecutor.ts
|
|
819
|
+
var import_node_child_process = require("node:child_process");
|
|
820
|
+
var import_zod2 = __toESM(require("zod"), 1);
|
|
809
821
|
var import_internal5 = require("@alumnium/core/internal");
|
|
822
|
+
var { logger: logger5 } = import_internal5.Telemetry.get(
|
|
823
|
+
"packages/typescript/test/src/player/BashExecutor.ts",
|
|
824
|
+
);
|
|
825
|
+
var BASH_TOOL = "Bash";
|
|
826
|
+
var DEFAULT_TIMEOUT_MS = 120000;
|
|
827
|
+
var BashInput = import_zod2.default.object({
|
|
828
|
+
command: import_zod2.default.string(),
|
|
829
|
+
timeout: import_zod2.default.number().positive().optional(),
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
class BashExecutor {
|
|
833
|
+
#props;
|
|
834
|
+
constructor(props) {
|
|
835
|
+
this.#props = props;
|
|
836
|
+
}
|
|
837
|
+
handles(call) {
|
|
838
|
+
return !call.server && call.tool === BASH_TOOL;
|
|
839
|
+
}
|
|
840
|
+
async execute(call) {
|
|
841
|
+
const { command, timeout } = BashInput.parse(call.input);
|
|
842
|
+
if (!this.#allows(command))
|
|
843
|
+
throw new Error(`Command is not allowed to replay: ${command}`);
|
|
844
|
+
logger5.debug(`Running command: ${command}`);
|
|
845
|
+
const { code, stdout, stderr } = await this.#run(command, {
|
|
846
|
+
cwd: this.#props.cwd,
|
|
847
|
+
env: { ...process.env, ...this.#props.env },
|
|
848
|
+
timeoutMs: timeout ?? this.#props.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
849
|
+
});
|
|
850
|
+
if (code === 0) return { content: stdout, isError: false };
|
|
851
|
+
return {
|
|
852
|
+
content: [stdout, stderr, `Exit code ${code}`].filter(Boolean).join(`
|
|
853
|
+
`),
|
|
854
|
+
isError: true,
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
#allows(command) {
|
|
858
|
+
return this.#props.allow.some((allowed) =>
|
|
859
|
+
typeof allowed === "string"
|
|
860
|
+
? command.startsWith(allowed)
|
|
861
|
+
: allowed.test(command),
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
#run(command, options) {
|
|
865
|
+
return new Promise((resolve, reject) => {
|
|
866
|
+
const child = import_node_child_process.spawn("bash", ["-c", command], {
|
|
867
|
+
cwd: options.cwd,
|
|
868
|
+
env: options.env,
|
|
869
|
+
timeout: options.timeoutMs,
|
|
870
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
871
|
+
});
|
|
872
|
+
let stdout = "";
|
|
873
|
+
let stderr = "";
|
|
874
|
+
child.stdout.setEncoding("utf8").on("data", (data) => (stdout += data));
|
|
875
|
+
child.stderr.setEncoding("utf8").on("data", (data) => (stderr += data));
|
|
876
|
+
child.on("error", reject);
|
|
877
|
+
child.on("close", (code) => resolve({ code, stdout, stderr }));
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
// test/src/player/McpExecutor.ts
|
|
882
|
+
var import_client = require("@modelcontextprotocol/sdk/client/index.js");
|
|
883
|
+
var import_stdio = require("@modelcontextprotocol/sdk/client/stdio.js");
|
|
884
|
+
var import_streamableHttp = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
885
|
+
var import_internal6 = require("@alumnium/core/internal");
|
|
886
|
+
var { logger: logger6 } = import_internal6.Telemetry.get(
|
|
887
|
+
"packages/typescript/test/src/player/McpExecutor.ts",
|
|
888
|
+
);
|
|
889
|
+
var DEFAULT_RETRIES = 2;
|
|
890
|
+
var DEFAULT_RETRY_DELAY_MS = 1000;
|
|
891
|
+
|
|
892
|
+
class McpExecutor {
|
|
893
|
+
#servers;
|
|
894
|
+
#retries;
|
|
895
|
+
#retryDelayMs;
|
|
896
|
+
#clients = new Map();
|
|
897
|
+
constructor(props) {
|
|
898
|
+
this.#servers = props.servers;
|
|
899
|
+
this.#retries = props.retries ?? DEFAULT_RETRIES;
|
|
900
|
+
this.#retryDelayMs = props.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
|
|
901
|
+
}
|
|
902
|
+
handles(call) {
|
|
903
|
+
return Boolean(call.server && call.server in this.#servers);
|
|
904
|
+
}
|
|
905
|
+
async execute(call, options = {}) {
|
|
906
|
+
const toolKey = Recording.toolKey(call);
|
|
907
|
+
const client = await this.#client(call.server);
|
|
908
|
+
for (let attempt = 0; ; attempt++) {
|
|
909
|
+
const canRetry = attempt < this.#retries;
|
|
910
|
+
const delayMs = this.#retryDelayMs * 2 ** attempt;
|
|
911
|
+
try {
|
|
912
|
+
const result = await client.callTool({
|
|
913
|
+
name: call.tool,
|
|
914
|
+
arguments: call.input,
|
|
915
|
+
...(options.meta ? { _meta: options.meta } : {}),
|
|
916
|
+
});
|
|
917
|
+
const toolResult = {
|
|
918
|
+
content: result.content ?? [],
|
|
919
|
+
isError: Boolean(result.isError),
|
|
920
|
+
};
|
|
921
|
+
if (!toolResult.isError || !canRetry) return toolResult;
|
|
922
|
+
logger6.warn(`Tool '${toolKey}' reported an error, retrying`);
|
|
923
|
+
} catch (error) {
|
|
924
|
+
if (!canRetry) throw error;
|
|
925
|
+
logger6.warn(`Tool '${toolKey}' failed, retrying: {error}`, { error });
|
|
926
|
+
}
|
|
927
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
async close() {
|
|
931
|
+
const clients = [...this.#clients];
|
|
932
|
+
this.#clients.clear();
|
|
933
|
+
await Promise.all(
|
|
934
|
+
clients.map(async ([name, client]) => {
|
|
935
|
+
if (this.#servers[name]?.type === "client") return;
|
|
936
|
+
try {
|
|
937
|
+
await (await client).close();
|
|
938
|
+
} catch (error) {
|
|
939
|
+
logger6.warn(`Failed to close MCP server '${name}': {error}`, {
|
|
940
|
+
error,
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
}),
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
#client(name) {
|
|
947
|
+
let client = this.#clients.get(name);
|
|
948
|
+
if (!client) {
|
|
949
|
+
client = this.#connect(name);
|
|
950
|
+
client.catch(() => this.#clients.delete(name));
|
|
951
|
+
this.#clients.set(name, client);
|
|
952
|
+
}
|
|
953
|
+
return client;
|
|
954
|
+
}
|
|
955
|
+
async #connect(name) {
|
|
956
|
+
const server = this.#servers[name];
|
|
957
|
+
if (!server) throw new Error(`Unknown MCP server '${name}'`);
|
|
958
|
+
if (server.type === "client") return server.client;
|
|
959
|
+
logger6.debug(`Connecting to MCP server '${name}'`);
|
|
960
|
+
const transport =
|
|
961
|
+
server.type === "http"
|
|
962
|
+
? new import_streamableHttp.StreamableHTTPClientTransport(
|
|
963
|
+
new URL(server.url),
|
|
964
|
+
{
|
|
965
|
+
requestInit: { headers: server.headers ?? {} },
|
|
966
|
+
},
|
|
967
|
+
)
|
|
968
|
+
: new import_stdio.StdioClientTransport({
|
|
969
|
+
command: server.command,
|
|
970
|
+
args: server.args ?? [],
|
|
971
|
+
env: { ...this.#definedEnv(), ...server.env },
|
|
972
|
+
...(server.cwd ? { cwd: server.cwd } : {}),
|
|
973
|
+
stderr: "inherit",
|
|
974
|
+
});
|
|
975
|
+
const client = new import_client.Client({
|
|
976
|
+
name: "alumnium-test-player",
|
|
977
|
+
version: import_internal6.ALUMNIUM_CORE_VERSION,
|
|
978
|
+
});
|
|
979
|
+
await client.connect(transport);
|
|
980
|
+
return client;
|
|
981
|
+
}
|
|
982
|
+
#definedEnv() {
|
|
983
|
+
return Object.fromEntries(
|
|
984
|
+
Object.entries(process.env).filter((entry) => entry[1] !== undefined),
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
// test/src/player/Player.ts
|
|
989
|
+
var import_zod3 = __toESM(require("zod"), 1);
|
|
990
|
+
var import_internal7 = require("@alumnium/core/internal");
|
|
991
|
+
|
|
992
|
+
// test/src/masker/alumniumMaskerProps.ts
|
|
993
|
+
var DEFAULT_ALUMNIUM_SERVER = "alumnium";
|
|
994
|
+
var ALUMNIUM_START_TOOL = "start";
|
|
995
|
+
function alumniumMaskerProps(props, server = DEFAULT_ALUMNIUM_SERVER) {
|
|
996
|
+
return {
|
|
997
|
+
...props,
|
|
998
|
+
registerResponsePaths: {
|
|
999
|
+
...alumniumRegisterResponsePaths(server),
|
|
1000
|
+
...props.registerResponsePaths,
|
|
1001
|
+
},
|
|
1002
|
+
wholeValueOnlyTools: [`${server}/*`, ...(props.wholeValueOnlyTools ?? [])],
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
function alumniumRegisterResponsePaths(server) {
|
|
1006
|
+
return {
|
|
1007
|
+
[`${server}/*`]: [],
|
|
1008
|
+
[`${server}/${ALUMNIUM_START_TOOL}`]: ["id"],
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// test/src/player/Player.ts
|
|
1013
|
+
var { logger: logger7 } = import_internal7.Telemetry.get(
|
|
1014
|
+
"packages/typescript/test/src/player/Player.ts",
|
|
1015
|
+
);
|
|
1016
|
+
var START_TOOL = "start";
|
|
1017
|
+
var STOP_TOOL = "stop";
|
|
1018
|
+
var CHECK_TOOL = "check";
|
|
1019
|
+
var NO_CACHE_META = { "alumnium.ai/cache": false };
|
|
1020
|
+
var NO_CHANGE_ANALYSIS_META = { "alumnium.ai/change-analysis": false };
|
|
1021
|
+
var DEFAULT_SKIP_TOOLS = ["ToolSearch", "TodoWrite"];
|
|
1022
|
+
var DriverOutput = import_zod3.default.object({
|
|
1023
|
+
id: import_zod3.default.string(),
|
|
1024
|
+
});
|
|
1025
|
+
var CheckOutput = import_zod3.default.object({
|
|
1026
|
+
result: import_zod3.default.enum(["success", "failure"]),
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
class Player {
|
|
1030
|
+
#options;
|
|
1031
|
+
#maskerProps;
|
|
1032
|
+
constructor(props) {
|
|
1033
|
+
const {
|
|
1034
|
+
executor,
|
|
1035
|
+
alumniumServer = DEFAULT_ALUMNIUM_SERVER,
|
|
1036
|
+
skipTools = DEFAULT_SKIP_TOOLS,
|
|
1037
|
+
checkConfirmations,
|
|
1038
|
+
observer = {},
|
|
1039
|
+
...maskerProps
|
|
1040
|
+
} = props;
|
|
1041
|
+
this.#options = {
|
|
1042
|
+
executor,
|
|
1043
|
+
server: alumniumServer,
|
|
1044
|
+
skipTools: new Set(skipTools),
|
|
1045
|
+
checkConfirmations,
|
|
1046
|
+
observer,
|
|
1047
|
+
};
|
|
1048
|
+
this.#maskerProps = alumniumMaskerProps(maskerProps, alumniumServer);
|
|
1049
|
+
}
|
|
1050
|
+
async play(recording) {
|
|
1051
|
+
const run = new PlayerRun(this.#options, new Masker(this.#maskerProps));
|
|
1052
|
+
try {
|
|
1053
|
+
return await run.play(recording);
|
|
1054
|
+
} finally {
|
|
1055
|
+
await run.stopDrivers();
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
class Diverged extends Error {}
|
|
1061
|
+
|
|
1062
|
+
class PlayerRun {
|
|
1063
|
+
#options;
|
|
1064
|
+
#masker;
|
|
1065
|
+
#driverIds = new Set();
|
|
1066
|
+
constructor(options, masker) {
|
|
1067
|
+
this.#options = options;
|
|
1068
|
+
this.#masker = masker;
|
|
1069
|
+
}
|
|
1070
|
+
get #observer() {
|
|
1071
|
+
return this.#options.observer;
|
|
1072
|
+
}
|
|
1073
|
+
async play(recording) {
|
|
1074
|
+
const stepsCount = Recording.executableStepsCount(recording);
|
|
1075
|
+
let stepNumber = 0;
|
|
1076
|
+
for (const [stepIndex, step] of recording.steps.entries()) {
|
|
1077
|
+
if (step.kind === "narration") {
|
|
1078
|
+
this.#observer.onNarration?.(step);
|
|
1079
|
+
continue;
|
|
1080
|
+
}
|
|
1081
|
+
logger7.info(`Replaying step ${++stepNumber}/${stepsCount}`);
|
|
1082
|
+
try {
|
|
1083
|
+
await this.#playToolStep(step);
|
|
1084
|
+
} catch (error) {
|
|
1085
|
+
const message = error instanceof Diverged ? error.message : `${error}`;
|
|
1086
|
+
logger7.error(`Replay diverged at step ${stepNumber}: ${message}`);
|
|
1087
|
+
return { status: "diverged", error: message, stepIndex };
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
logger7.info(`Replayed all ${stepsCount} steps`);
|
|
1091
|
+
return { status: "replayed", verdict: recording.verdict };
|
|
1092
|
+
}
|
|
1093
|
+
async stopDrivers() {
|
|
1094
|
+
for (const id of this.#driverIds) {
|
|
1095
|
+
try {
|
|
1096
|
+
logger7.info(`Stopping driver ${id} the replay did not stop`);
|
|
1097
|
+
await this.#options.executor.execute({
|
|
1098
|
+
server: this.#options.server,
|
|
1099
|
+
tool: STOP_TOOL,
|
|
1100
|
+
input: { id, save_cache: false },
|
|
1101
|
+
});
|
|
1102
|
+
} catch (error) {
|
|
1103
|
+
logger7.warn(`Failed to stop driver ${id}: {error}`, { error });
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
this.#driverIds.clear();
|
|
1107
|
+
}
|
|
1108
|
+
async #playToolStep(step) {
|
|
1109
|
+
const { call: recordedCall, result: recordedResult } = step;
|
|
1110
|
+
const { id, ...callProps } = recordedCall;
|
|
1111
|
+
const toolKey = Recording.toolKey(recordedCall);
|
|
1112
|
+
if (recordedResult.isError)
|
|
1113
|
+
return this.#skip(recordedCall, "it failed when recorded");
|
|
1114
|
+
if (!this.#options.executor.handles(callProps)) {
|
|
1115
|
+
if (this.#options.skipTools.has(toolKey))
|
|
1116
|
+
return this.#skip(recordedCall, "it only exists inside the agent");
|
|
1117
|
+
throw new Diverged(`No executor for tool '${toolKey}'`);
|
|
1118
|
+
}
|
|
1119
|
+
const call = {
|
|
1120
|
+
...callProps,
|
|
1121
|
+
input: this.#masker.unmaskInput(toolKey, recordedCall.input),
|
|
1122
|
+
};
|
|
1123
|
+
const unresolved = Masker.findUnresolvedMasks(call.input);
|
|
1124
|
+
if (unresolved.length)
|
|
1125
|
+
throw new Diverged(
|
|
1126
|
+
`Tool '${toolKey}' input has unresolved masks: ${unresolved.join(", ")}`,
|
|
1127
|
+
);
|
|
1128
|
+
const result = await this.#execute(
|
|
1129
|
+
call,
|
|
1130
|
+
call.server === this.#options.server && call.tool === START_TOOL
|
|
1131
|
+
? { meta: NO_CHANGE_ANALYSIS_META }
|
|
1132
|
+
: undefined,
|
|
1133
|
+
);
|
|
1134
|
+
if (result.isError)
|
|
1135
|
+
throw new Diverged(
|
|
1136
|
+
`Tool '${toolKey}' failed: ${contentTexts(result.content).join(`
|
|
1137
|
+
`)}`,
|
|
1138
|
+
);
|
|
1139
|
+
this.#masker.registerOutput(id, result.content, toolKey);
|
|
1140
|
+
if (call.server !== this.#options.server) return;
|
|
1141
|
+
switch (call.tool) {
|
|
1142
|
+
case START_TOOL: {
|
|
1143
|
+
const driverId = this.#parseOutput(result.content, DriverOutput)?.id;
|
|
1144
|
+
if (driverId) this.#driverIds.add(driverId);
|
|
1145
|
+
break;
|
|
1146
|
+
}
|
|
1147
|
+
case STOP_TOOL: {
|
|
1148
|
+
const driverId = DriverOutput.safeParse(call.input).data?.id;
|
|
1149
|
+
if (driverId) this.#driverIds.delete(driverId);
|
|
1150
|
+
break;
|
|
1151
|
+
}
|
|
1152
|
+
case CHECK_TOOL:
|
|
1153
|
+
await this.#compareCheck(call, recordedResult, result);
|
|
1154
|
+
break;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
async #execute(call, options) {
|
|
1158
|
+
this.#observer.onToolCall?.(call);
|
|
1159
|
+
const result = await this.#options.executor.execute(call, options);
|
|
1160
|
+
this.#observer.onToolResult?.(call, result);
|
|
1161
|
+
return result;
|
|
1162
|
+
}
|
|
1163
|
+
#skip(call, reason) {
|
|
1164
|
+
logger7.info(`Skipping tool '${Recording.toolKey(call)}', ${reason}`);
|
|
1165
|
+
this.#observer.onToolSkipped?.(call, reason);
|
|
1166
|
+
}
|
|
1167
|
+
async #compareCheck(call, recorded, replayed) {
|
|
1168
|
+
const confirmations =
|
|
1169
|
+
this.#options.checkConfirmations ??
|
|
1170
|
+
import_internal7.Env.ALUMNIUM_CHECK_CONFIRMATIONS;
|
|
1171
|
+
const expected = this.#checkVerdict(recorded);
|
|
1172
|
+
let actual = this.#checkVerdict(replayed);
|
|
1173
|
+
let outcome;
|
|
1174
|
+
if (!expected || !actual) {
|
|
1175
|
+
throw new Diverged(
|
|
1176
|
+
`Cannot read '${Recording.toolKey(call)}' verdict to compare`,
|
|
1177
|
+
);
|
|
1178
|
+
} else if (expected === actual) {
|
|
1179
|
+
outcome = "agreed";
|
|
1180
|
+
} else if (expected === "failure") {
|
|
1181
|
+
outcome = "improved";
|
|
1182
|
+
} else {
|
|
1183
|
+
let attempt = 0;
|
|
1184
|
+
while (actual !== expected && attempt++ < confirmations) {
|
|
1185
|
+
logger7.info(`Re-asking check ${attempt}/${confirmations}`);
|
|
1186
|
+
const result = await this.#execute(call, { meta: NO_CACHE_META });
|
|
1187
|
+
actual = result.isError ? null : this.#checkVerdict(result);
|
|
1188
|
+
}
|
|
1189
|
+
outcome = actual === expected ? "unstable" : "disagreed";
|
|
1190
|
+
}
|
|
1191
|
+
this.#observer.onCheck?.(call, outcome);
|
|
1192
|
+
if (outcome === "disagreed")
|
|
1193
|
+
throw new Diverged(
|
|
1194
|
+
`Check '${call.input["statement"]}' failed where it passed when recorded` +
|
|
1195
|
+
(confirmations
|
|
1196
|
+
? `, re-asked ${confirmations} times with the cache bypassed`
|
|
1197
|
+
: ""),
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
#checkVerdict(result) {
|
|
1201
|
+
return this.#parseOutput(result.content, CheckOutput)?.result ?? null;
|
|
1202
|
+
}
|
|
1203
|
+
#parseOutput(content, schema) {
|
|
1204
|
+
for (const text of contentTexts(content)) {
|
|
1205
|
+
const parsed = schema.safeParse(parseJson(text));
|
|
1206
|
+
if (parsed.success) return parsed.data;
|
|
1207
|
+
}
|
|
1208
|
+
return null;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
// test/src/player/ToolExecutor.ts
|
|
1212
|
+
function composeExecutors(...executors) {
|
|
1213
|
+
const find = (call) => executors.find((executor) => executor.handles(call));
|
|
1214
|
+
return {
|
|
1215
|
+
handles: (call) => Boolean(find(call)),
|
|
1216
|
+
async execute(call, options) {
|
|
1217
|
+
const executor = find(call);
|
|
1218
|
+
if (!executor)
|
|
1219
|
+
throw new Error(
|
|
1220
|
+
`No executor handles tool '${Recording.toolKey(call)}'`,
|
|
1221
|
+
);
|
|
1222
|
+
return executor.execute(call, options);
|
|
1223
|
+
},
|
|
1224
|
+
async close() {
|
|
1225
|
+
await Promise.all(
|
|
1226
|
+
executors.map(async (executor) => await executor.close?.()),
|
|
1227
|
+
);
|
|
1228
|
+
},
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
// test/src/Recorder.ts
|
|
1232
|
+
var import_internal8 = require("@alumnium/core/internal");
|
|
810
1233
|
|
|
811
1234
|
// test/src/RecordingLeakError.ts
|
|
812
1235
|
class RecordingLeakError extends Error {
|
|
@@ -822,11 +1245,9 @@ class RecordingLeakError extends Error {
|
|
|
822
1245
|
}
|
|
823
1246
|
|
|
824
1247
|
// test/src/Recorder.ts
|
|
825
|
-
var { logger:
|
|
1248
|
+
var { logger: logger8 } = import_internal8.Telemetry.get(
|
|
826
1249
|
"packages/typescript/test/src/Recorder.ts",
|
|
827
1250
|
);
|
|
828
|
-
var DEFAULT_ALUMNIUM_SERVER = "alumnium";
|
|
829
|
-
var ALUMNIUM_START_TOOL = "start";
|
|
830
1251
|
|
|
831
1252
|
class Recorder {
|
|
832
1253
|
#text;
|
|
@@ -841,24 +1262,13 @@ class Recorder {
|
|
|
841
1262
|
this.#version = version ?? Recording.DEFAULT_VERSION;
|
|
842
1263
|
this.#text = text;
|
|
843
1264
|
this.#agent = agent;
|
|
844
|
-
|
|
845
|
-
this.#masker = new Masker({
|
|
846
|
-
...maskerProps,
|
|
847
|
-
registerResponsePaths: {
|
|
848
|
-
...this.#alumniumRegisterResponsePaths(server),
|
|
849
|
-
...maskerProps.registerResponsePaths,
|
|
850
|
-
},
|
|
851
|
-
wholeValueOnlyTools: [
|
|
852
|
-
`${server}/*`,
|
|
853
|
-
...(maskerProps.wholeValueOnlyTools ?? []),
|
|
854
|
-
],
|
|
855
|
-
});
|
|
1265
|
+
this.#masker = new Masker(alumniumMaskerProps(maskerProps, alumniumServer));
|
|
856
1266
|
}
|
|
857
1267
|
onToolUse(use) {
|
|
858
1268
|
this.#assertNotFinished();
|
|
859
1269
|
const { id, server, tool, input } = use;
|
|
860
1270
|
const toolKey = Recording.toolKey({ server, tool });
|
|
861
|
-
|
|
1271
|
+
logger8.debug(`Recording tool use '${toolKey}' ({id})`, { id });
|
|
862
1272
|
this.#pendingCalls.set(id, {
|
|
863
1273
|
id,
|
|
864
1274
|
...(server ? { server } : {}),
|
|
@@ -872,11 +1282,11 @@ class Recorder {
|
|
|
872
1282
|
const isError = Boolean(toolResult.isError);
|
|
873
1283
|
const call = this.#pendingCalls.get(id);
|
|
874
1284
|
if (!call) {
|
|
875
|
-
|
|
1285
|
+
logger8.debug(`No recorded tool use for result '${id}', ignoring it`);
|
|
876
1286
|
return;
|
|
877
1287
|
}
|
|
878
1288
|
this.#pendingCalls.delete(id);
|
|
879
|
-
|
|
1289
|
+
logger8.debug(`Recording '${call.tool}' tool result`);
|
|
880
1290
|
this.#masker.registerOutput(id, content, Recording.toolKey(call), {
|
|
881
1291
|
secretsOnly: isError,
|
|
882
1292
|
});
|
|
@@ -898,7 +1308,7 @@ class Recorder {
|
|
|
898
1308
|
finish(verdict) {
|
|
899
1309
|
this.#assertNotFinished();
|
|
900
1310
|
if (this.#pendingCalls.size)
|
|
901
|
-
|
|
1311
|
+
logger8.warn(
|
|
902
1312
|
`Dropping ${this.#pendingCalls.size} tool calls with no result: ${[
|
|
903
1313
|
...this.#pendingCalls.values(),
|
|
904
1314
|
]
|
|
@@ -911,7 +1321,7 @@ class Recorder {
|
|
|
911
1321
|
version: this.#version,
|
|
912
1322
|
text: this.#text,
|
|
913
1323
|
agent: this.#agent,
|
|
914
|
-
alumnium: { version:
|
|
1324
|
+
alumnium: { version: import_internal8.ALUMNIUM_CORE_VERSION },
|
|
915
1325
|
hostVars: [],
|
|
916
1326
|
steps: this.#steps.map((step) => this.#maskStep(step)),
|
|
917
1327
|
verdict: {
|
|
@@ -946,16 +1356,10 @@ class Recorder {
|
|
|
946
1356
|
};
|
|
947
1357
|
}
|
|
948
1358
|
}
|
|
949
|
-
#alumniumRegisterResponsePaths(server = DEFAULT_ALUMNIUM_SERVER) {
|
|
950
|
-
return {
|
|
951
|
-
[`${server}/*`]: [],
|
|
952
|
-
[`${server}/${ALUMNIUM_START_TOOL}`]: ["id"],
|
|
953
|
-
};
|
|
954
|
-
}
|
|
955
1359
|
#assertNotFinished() {
|
|
956
1360
|
if (this.#recording) throw new Error("Recording is already finished");
|
|
957
1361
|
}
|
|
958
1362
|
}
|
|
959
1363
|
|
|
960
|
-
//# debugId=
|
|
1364
|
+
//# debugId=18EDAFFEFAD0500964756E2164756E21
|
|
961
1365
|
//# sourceMappingURL=index.cjs.map
|