@drkmttr/adhere 0.15.0 → 0.18.0
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 +313 -131
- package/bin/adhere.js +19 -4
- package/package.json +34 -12
- package/skills/adhere/SKILL.md +180 -31
- package/skills/adhere-setup/SKILL.md +142 -16
- package/src/cli.ts +56 -23
- package/src/config.ts +62 -16
- package/src/index.ts +14 -0
- package/src/markdown.ts +9 -5
- package/src/providers/cloudflare.ts +84 -0
- package/src/providers/jev.ts +66 -0
- package/src/providers/openai.ts +89 -0
- package/src/providers/post.ts +53 -0
- package/src/providers/provider.ts +131 -0
- package/src/providers/providers.test.ts +276 -0
- package/src/providers/systemOne.ts +95 -0
- package/src/services/AdhereConfig.ts +2 -4
- package/src/services/Credentials.ts +101 -51
- package/src/services/Jev.http.ts +184 -171
- package/src/services/Jev.provider.test.ts +483 -0
- package/src/services/Jev.reads.test.ts +516 -0
- package/src/services/Jev.ts +276 -120
- package/src/services/NativeLsp.ts +289 -0
- package/src/services/SourceReads.test.ts +1289 -0
- package/src/services/SourceReads.ts +460 -0
- package/src/services/SourceWalker.ts +26 -59
- package/src/workflows/audit.ts +513 -267
- package/src/workflows/format.ts +74 -17
- package/src/workspaces.test.ts +73 -0
- package/src/workspaces.ts +91 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
import { decodeConfig, resolveConfig, type JevState, type Rule, type Rules } from "#config.ts";
|
|
2
|
+
import {
|
|
3
|
+
type AskOptions,
|
|
4
|
+
ContextOverflow,
|
|
5
|
+
type Provider,
|
|
6
|
+
type Question,
|
|
7
|
+
RequestBlocked,
|
|
8
|
+
RequestFailed,
|
|
9
|
+
type SavedKey,
|
|
10
|
+
type State,
|
|
11
|
+
} from "#providers/provider.ts";
|
|
12
|
+
import { AdhereConfig } from "#services/AdhereConfig.ts";
|
|
13
|
+
import { Credentials } from "#services/Credentials.ts";
|
|
14
|
+
import { JevLive } from "#services/Jev.http.ts";
|
|
15
|
+
import { describeState, Jev, judgeBody, locateBody, requestsOf } from "#services/Jev.ts";
|
|
16
|
+
import { questionsOf, requestOf, type SystemOneRequest } from "#providers/systemOne.ts";
|
|
17
|
+
import { Effect, Layer, Logger, Redacted, References } from "effect";
|
|
18
|
+
import { FetchHttpClient } from "effect/http";
|
|
19
|
+
import { describe, expect, it } from "vite-plus/test";
|
|
20
|
+
|
|
21
|
+
const plain: Rule = { description: "Ports must be named.", must: "const port = PORT;" };
|
|
22
|
+
const named: Rule = {
|
|
23
|
+
description: "Handlers must be named.",
|
|
24
|
+
must: "export const handle = () => 1;",
|
|
25
|
+
};
|
|
26
|
+
const lines = ["const port = 3000;"];
|
|
27
|
+
const file = { path: "/repo/src/server.ts", contents: lines.join("\n") };
|
|
28
|
+
|
|
29
|
+
// Restore JSON escapes only inside string tokens, leaving layout and source backslashes intact.
|
|
30
|
+
const jsonFromDiagnostic = (text: string): string =>
|
|
31
|
+
text.replace(/"(?:\\[\s\S]|[^"\\])*"/g, (token) =>
|
|
32
|
+
token.replace(/\n/g, "\\n").replace(/\t/g, "\\t"),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Jev under a config whose provider answers: any request Jev's own provider
|
|
37
|
+
* sends, or any read of its key, dies. The saved OpenAI key is "sk-saved".
|
|
38
|
+
*/
|
|
39
|
+
const judged = (
|
|
40
|
+
ask: Provider["ask"],
|
|
41
|
+
savedKey?: SavedKey,
|
|
42
|
+
rules: Rules = { plain, named },
|
|
43
|
+
provider: Provider = {
|
|
44
|
+
model: "clef-flash",
|
|
45
|
+
ask,
|
|
46
|
+
...(savedKey === undefined ? {} : { savedKey }),
|
|
47
|
+
},
|
|
48
|
+
kind: "judge" | "locate" = "judge",
|
|
49
|
+
) =>
|
|
50
|
+
Effect.gen(function* () {
|
|
51
|
+
const jev = yield* Jev;
|
|
52
|
+
return kind === "judge"
|
|
53
|
+
? yield* jev.judge(lines, rules, [], file)
|
|
54
|
+
: yield* jev.locate(lines, rules, file);
|
|
55
|
+
}).pipe(
|
|
56
|
+
Effect.provide(
|
|
57
|
+
JevLive.pipe(
|
|
58
|
+
Layer.provide([
|
|
59
|
+
Layer.succeed(FetchHttpClient.Fetch, (() => {
|
|
60
|
+
throw new Error("a provider sends nothing to Jev");
|
|
61
|
+
}) as unknown as typeof globalThis.fetch),
|
|
62
|
+
Layer.succeed(
|
|
63
|
+
AdhereConfig,
|
|
64
|
+
resolveConfig({
|
|
65
|
+
presets: [],
|
|
66
|
+
rules: {},
|
|
67
|
+
model: "jev-latest",
|
|
68
|
+
provider,
|
|
69
|
+
}),
|
|
70
|
+
),
|
|
71
|
+
Layer.succeed(Credentials, {
|
|
72
|
+
key: (name) =>
|
|
73
|
+
name === "openai"
|
|
74
|
+
? Effect.succeed(Redacted.make("sk-saved"))
|
|
75
|
+
: Effect.die("a provider needs no TypeSafe key"),
|
|
76
|
+
file: Effect.die("unused"),
|
|
77
|
+
save: () => Effect.die("unused"),
|
|
78
|
+
remove: () => Effect.die("unused"),
|
|
79
|
+
}),
|
|
80
|
+
]),
|
|
81
|
+
),
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
describe("a config's provider", () => {
|
|
86
|
+
it("is asked adhere's questions, split into their parts, about the file's state", async () => {
|
|
87
|
+
const asked: Array<{ readonly state: State; readonly questions: ReadonlyArray<Question> }> = [];
|
|
88
|
+
const result = await Effect.runPromise(
|
|
89
|
+
judged(async (state, questions) => {
|
|
90
|
+
asked.push({ state, questions });
|
|
91
|
+
return { answers: { plain: 0.93 } };
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
const [{ state, questions } = { state: {}, questions: [] }] = asked;
|
|
95
|
+
expect(state).toEqual({ code: { "1": "const port = 3000;" } });
|
|
96
|
+
expect(questions.map((question) => question.id)).toEqual(["plain", "named"]);
|
|
97
|
+
expect(questions[0]).toMatchObject({
|
|
98
|
+
type: "noul",
|
|
99
|
+
question: expect.stringContaining("Does `code` diverge from the pattern shown in `must`"),
|
|
100
|
+
fields: { rule: "Ports must be named.", must: "const port = PORT;" },
|
|
101
|
+
yes: expect.stringContaining("`code` diverges from the pattern shown in `must`"),
|
|
102
|
+
no: expect.stringContaining("`code` follows the pattern"),
|
|
103
|
+
});
|
|
104
|
+
// A question the provider leaves out goes unanswered, as when Jev gives no answer.
|
|
105
|
+
expect(result.probabilities).toEqual({ plain: 0.93 });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("is asked with the key `adhere login` saved for it, when it names one", async () => {
|
|
109
|
+
const keys: Array<string | undefined> = [];
|
|
110
|
+
const ask: Provider["ask"] = async (_state, _questions, { apiKey }) => {
|
|
111
|
+
keys.push(apiKey);
|
|
112
|
+
return { answers: {} };
|
|
113
|
+
};
|
|
114
|
+
await Effect.runPromise(judged(ask, "openai"));
|
|
115
|
+
await Effect.runPromise(judged(ask));
|
|
116
|
+
expect(keys).toEqual(["sk-saved", undefined]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("skips the file when the request is over its context, and reports it blocked when it is blocked", async () => {
|
|
120
|
+
const overflowed = await Effect.runPromise(
|
|
121
|
+
Effect.flip(judged(() => Promise.reject(new ContextOverflow("too long")))),
|
|
122
|
+
);
|
|
123
|
+
expect(overflowed._tag).toBe("JevOverflow");
|
|
124
|
+
const blocked = await Effect.runPromise(
|
|
125
|
+
Effect.flip(judged(() => Promise.reject(new RequestBlocked("ref-7")))),
|
|
126
|
+
);
|
|
127
|
+
expect(blocked).toMatchObject({ _tag: "JevBlocked", ray: "ref-7" });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it.each([
|
|
131
|
+
["All", "judge"],
|
|
132
|
+
["Trace", "judge"],
|
|
133
|
+
["Debug", "judge"],
|
|
134
|
+
["All", "locate"],
|
|
135
|
+
["Trace", "locate"],
|
|
136
|
+
["Debug", "locate"],
|
|
137
|
+
] as const)(
|
|
138
|
+
"logs split requests after appendState with line criteria only at All (minimum: %s, kind: %s)",
|
|
139
|
+
async (minimum, kind) => {
|
|
140
|
+
const context = `export function caller() {\n\tconst escaped = "\\n\\t";\n\treturn "${"hook-context".repeat(40_000)}";\n}`;
|
|
141
|
+
const added = { references: { "port:caller.ts": context } };
|
|
142
|
+
const hooked: Rule = {
|
|
143
|
+
...plain,
|
|
144
|
+
appliesTo: ["a server"],
|
|
145
|
+
appendState: async () => added,
|
|
146
|
+
};
|
|
147
|
+
const body = (
|
|
148
|
+
kind === "judge"
|
|
149
|
+
? judgeBody("clef-flash", lines, { plain: hooked })
|
|
150
|
+
: locateBody("clef-flash", lines, { plain: hooked })
|
|
151
|
+
) as SystemOneRequest & { readonly state: JevState };
|
|
152
|
+
const requests = requestsOf({ ...body, state: describeState({ ...body.state, ...added }) });
|
|
153
|
+
expect(requests.length).toBeGreaterThan(1);
|
|
154
|
+
const request = requests[0]!;
|
|
155
|
+
const asked: Array<{
|
|
156
|
+
state: State;
|
|
157
|
+
questions: ReadonlyArray<Question>;
|
|
158
|
+
apiKey: string | undefined;
|
|
159
|
+
}> = [];
|
|
160
|
+
let originalBody = "";
|
|
161
|
+
const originalAdded = JSON.stringify(added);
|
|
162
|
+
const logs: Array<Logger.Options<unknown>> = [];
|
|
163
|
+
const overflowed = await Effect.runPromise(
|
|
164
|
+
Effect.flip(
|
|
165
|
+
judged(
|
|
166
|
+
async (state, questions, { apiKey }) => {
|
|
167
|
+
asked.push({ state, questions, apiKey });
|
|
168
|
+
originalBody = JSON.stringify(requestOf("clef-flash", state, questions));
|
|
169
|
+
throw new ContextOverflow("too long");
|
|
170
|
+
},
|
|
171
|
+
"openai",
|
|
172
|
+
{ plain: hooked },
|
|
173
|
+
undefined,
|
|
174
|
+
kind,
|
|
175
|
+
),
|
|
176
|
+
).pipe(
|
|
177
|
+
Effect.provide(Logger.layer([Logger.make((entry) => logs.push(entry))])),
|
|
178
|
+
Effect.provideService(References.MinimumLogLevel, minimum),
|
|
179
|
+
),
|
|
180
|
+
);
|
|
181
|
+
expect(overflowed._tag).toBe("JevOverflow");
|
|
182
|
+
expect(asked).toEqual([
|
|
183
|
+
{ state: request.state, questions: questionsOf(request.questions), apiKey: "sk-saved" },
|
|
184
|
+
]);
|
|
185
|
+
const messages = logs.flatMap(({ message }) =>
|
|
186
|
+
Array.isArray(message) ? message : [message],
|
|
187
|
+
);
|
|
188
|
+
const oversized = messages.filter((message) =>
|
|
189
|
+
String(message).includes("too long request payload"),
|
|
190
|
+
);
|
|
191
|
+
if (minimum !== "Debug") {
|
|
192
|
+
const label = `${kind}: too long request payload (provider overflow)\n`;
|
|
193
|
+
expect(oversized).toHaveLength(1);
|
|
194
|
+
expect(String(oversized[0]).startsWith(label)).toBe(true);
|
|
195
|
+
const json = jsonFromDiagnostic(String(oversized[0]).slice(label.length));
|
|
196
|
+
const logged = JSON.parse(json) as typeof request;
|
|
197
|
+
const id = kind === "judge" ? "plain" : "start:plain:1";
|
|
198
|
+
expect(Object.keys(request.questions)).toEqual([id]);
|
|
199
|
+
const question = request.questions[id]!;
|
|
200
|
+
expect(question.instructions).toMatchObject({ rule: plain.description, must: plain.must });
|
|
201
|
+
if (minimum === "Trace" && question.type === "choice") {
|
|
202
|
+
const { criteria, ...withoutCriteria } = question;
|
|
203
|
+
expect(criteria).toEqual({ "1": lines[0] });
|
|
204
|
+
expect(logged.questions[id]).not.toHaveProperty("criteria");
|
|
205
|
+
expect(logged).toEqual({ ...request, questions: { [id]: withoutCriteria } });
|
|
206
|
+
expect(json).toBe(JSON.stringify(logged, null, 2));
|
|
207
|
+
} else {
|
|
208
|
+
expect(json).toBe(JSON.stringify(request, null, 2));
|
|
209
|
+
expect(logged).toEqual(request);
|
|
210
|
+
expect(logged.questions[id]?.criteria).toBeDefined();
|
|
211
|
+
}
|
|
212
|
+
expect(logged.state).toMatchObject(added);
|
|
213
|
+
if (minimum === "Trace") expect(Object.keys(logged.state)[0]).toBe("code");
|
|
214
|
+
expect(logged.model).toBe("clef-flash");
|
|
215
|
+
expect(
|
|
216
|
+
logs.find(({ message }) => Array.isArray(message) && message.includes(oversized[0]))
|
|
217
|
+
?.logLevel,
|
|
218
|
+
).toBe("Trace");
|
|
219
|
+
} else {
|
|
220
|
+
expect(oversized).toEqual([]);
|
|
221
|
+
const label = `${kind}: first ${kind === "locate" ? "match-to-lines " : ""}request payload\n`;
|
|
222
|
+
const previews = messages.filter((message) => String(message).startsWith(label));
|
|
223
|
+
expect(previews).toHaveLength(1);
|
|
224
|
+
const logged = JSON.parse(
|
|
225
|
+
jsonFromDiagnostic(String(previews[0]).slice(label.length)),
|
|
226
|
+
) as SystemOneRequest;
|
|
227
|
+
expect(logged.state).toEqual(request.state);
|
|
228
|
+
expect(Object.keys(logged.state)[0]).toBe("code");
|
|
229
|
+
expect(logged.model).toBe(request.model);
|
|
230
|
+
const id = kind === "judge" ? "plain" : "start:plain:1";
|
|
231
|
+
if (kind === "locate") {
|
|
232
|
+
const { criteria, ...question } = request.questions[id]!;
|
|
233
|
+
expect(criteria).toBeDefined();
|
|
234
|
+
expect(logged.questions).toEqual({ [id]: question });
|
|
235
|
+
} else {
|
|
236
|
+
expect(logged.questions).toEqual(request.questions);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
for (const message of messages.filter((message) =>
|
|
240
|
+
String(message).includes("request payload"),
|
|
241
|
+
)) {
|
|
242
|
+
expect(String(message)).toContain("\n\tconst escaped = ");
|
|
243
|
+
expect(String(message)).toContain(String.raw`\\n\\t`);
|
|
244
|
+
expect(String(message)).not.toContain(String.raw`\n\tconst escaped`);
|
|
245
|
+
}
|
|
246
|
+
expect(originalBody).toContain(String.raw`\n\tconst escaped`);
|
|
247
|
+
expect(JSON.stringify(added)).toBe(originalAdded);
|
|
248
|
+
const sent = asked[0]!;
|
|
249
|
+
expect(JSON.stringify(requestOf("clef-flash", sent.state, sent.questions))).toBe(
|
|
250
|
+
originalBody,
|
|
251
|
+
);
|
|
252
|
+
expect(JSON.parse(originalBody)).toEqual(request);
|
|
253
|
+
expect(JSON.stringify(messages)).not.toContain("sk-saved");
|
|
254
|
+
expect(request).not.toHaveProperty("apiKey");
|
|
255
|
+
expect(request).not.toHaveProperty("signal");
|
|
256
|
+
expect(messages).toContain("attempt failed: too long");
|
|
257
|
+
},
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
it.each(["All", "Trace", "Debug", "Info"] as const)(
|
|
261
|
+
"previews only the first judge and match-to-lines request per shared JevLive (minimum: %s)",
|
|
262
|
+
async (minimum) => {
|
|
263
|
+
const context = `export function caller() {\n\tconst escaped = "\\n\\t";\n\treturn "${"preview-context".repeat(40_000)}";\n}`;
|
|
264
|
+
const added = { references: { "port:caller.ts": context } };
|
|
265
|
+
const readState = { workspacePackages: { server: "src" } };
|
|
266
|
+
const hookedRules = {
|
|
267
|
+
plain: { ...plain, appliesTo: ["a server"], appendState: async () => added },
|
|
268
|
+
named: { ...named, appliesTo: ["a server"], appendState: async () => added },
|
|
269
|
+
};
|
|
270
|
+
const files = [file, { path: "/repo/src/other.ts", contents: " const port = 8080;" }, file];
|
|
271
|
+
const expected = files.flatMap((current) => {
|
|
272
|
+
const code = current.contents.split("\n");
|
|
273
|
+
return ["judge", "locate"].flatMap((kind) =>
|
|
274
|
+
Object.entries(hookedRules).flatMap(([id, rule]) => {
|
|
275
|
+
const body = (
|
|
276
|
+
kind === "judge"
|
|
277
|
+
? judgeBody("clef-flash", code, { [id]: rule }, [], readState)
|
|
278
|
+
: locateBody("clef-flash", code, { [id]: rule }, readState)
|
|
279
|
+
) as SystemOneRequest & { readonly state: JevState };
|
|
280
|
+
const split = requestsOf({
|
|
281
|
+
...body,
|
|
282
|
+
state: describeState({ ...body.state, ...added }),
|
|
283
|
+
});
|
|
284
|
+
expect(split.length).toBeGreaterThan(1);
|
|
285
|
+
return split;
|
|
286
|
+
}),
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
const firstJudge = expected[0]!;
|
|
290
|
+
const firstLocate = expected.find((body) =>
|
|
291
|
+
Object.keys(body.questions)[0]?.startsWith("start:"),
|
|
292
|
+
)!;
|
|
293
|
+
const asked: Array<SystemOneRequest> = [];
|
|
294
|
+
const snapshots: string[] = [];
|
|
295
|
+
const keys: Array<string | undefined> = [];
|
|
296
|
+
const logs: Array<Logger.Options<unknown>> = [];
|
|
297
|
+
const provider: Provider = {
|
|
298
|
+
model: "clef-flash",
|
|
299
|
+
savedKey: "openai",
|
|
300
|
+
ask: async (state, questions, { apiKey }) => {
|
|
301
|
+
const body = requestOf("clef-flash", state, questions);
|
|
302
|
+
asked.push(body);
|
|
303
|
+
snapshots.push(JSON.stringify(body));
|
|
304
|
+
keys.push(apiKey);
|
|
305
|
+
if (asked.length === 1) throw new RequestFailed("retry first request", 503);
|
|
306
|
+
return { answers: {} };
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
await Effect.runPromise(
|
|
310
|
+
Effect.gen(function* () {
|
|
311
|
+
const jev = yield* Jev;
|
|
312
|
+
for (const current of files) {
|
|
313
|
+
const code = current.contents.split("\n");
|
|
314
|
+
const judged = yield* jev.judge(code, hookedRules, [], current, readState);
|
|
315
|
+
expect(judged.probabilities).toEqual({});
|
|
316
|
+
expect(yield* jev.locate(code, hookedRules, current, readState)).toEqual({});
|
|
317
|
+
}
|
|
318
|
+
}).pipe(
|
|
319
|
+
Effect.provide(
|
|
320
|
+
JevLive.pipe(
|
|
321
|
+
Layer.provide([
|
|
322
|
+
Layer.succeed(FetchHttpClient.Fetch, (() => {
|
|
323
|
+
throw new Error("preview tests send no HTTP requests");
|
|
324
|
+
}) as unknown as typeof globalThis.fetch),
|
|
325
|
+
Layer.succeed(AdhereConfig, resolveConfig({ presets: [], rules: {}, provider })),
|
|
326
|
+
Layer.succeed(Credentials, {
|
|
327
|
+
key: () => Effect.succeed(Redacted.make("sk-saved")),
|
|
328
|
+
file: Effect.die("unused"),
|
|
329
|
+
save: () => Effect.die("unused"),
|
|
330
|
+
remove: () => Effect.die("unused"),
|
|
331
|
+
}),
|
|
332
|
+
]),
|
|
333
|
+
),
|
|
334
|
+
),
|
|
335
|
+
Effect.provide(Logger.layer([Logger.make((entry) => logs.push(entry))])),
|
|
336
|
+
Effect.provideService(References.MinimumLogLevel, minimum),
|
|
337
|
+
),
|
|
338
|
+
);
|
|
339
|
+
expect(asked).toEqual([firstJudge, ...expected]);
|
|
340
|
+
expect(asked.map((body) => JSON.stringify(body))).toEqual(snapshots);
|
|
341
|
+
expect(snapshots[0]).toContain(String.raw`\n\tconst escaped`);
|
|
342
|
+
expect(snapshots.map((json) => JSON.parse(json))).toEqual(asked);
|
|
343
|
+
expect(keys).toEqual(asked.map(() => "sk-saved"));
|
|
344
|
+
const messages = logs.flatMap(({ message }) =>
|
|
345
|
+
Array.isArray(message) ? message : [message],
|
|
346
|
+
);
|
|
347
|
+
const previews = messages.filter((message) => String(message).includes("request payload"));
|
|
348
|
+
if (minimum === "Info") {
|
|
349
|
+
expect(previews).toEqual([]);
|
|
350
|
+
expect(JSON.stringify(messages)).not.toContain("preview-context");
|
|
351
|
+
} else {
|
|
352
|
+
const labels = [
|
|
353
|
+
"judge: first request payload\n",
|
|
354
|
+
"locate: first match-to-lines request payload\n",
|
|
355
|
+
];
|
|
356
|
+
expect(previews).toHaveLength(2);
|
|
357
|
+
for (const [index, body] of [firstJudge, firstLocate].entries()) {
|
|
358
|
+
const label = labels[index]!;
|
|
359
|
+
expect(String(previews[index]).startsWith(label)).toBe(true);
|
|
360
|
+
const text = String(previews[index]).slice(label.length);
|
|
361
|
+
expect(text).toContain("\n\tconst escaped = ");
|
|
362
|
+
expect(text).toContain(String.raw`\\n\\t`);
|
|
363
|
+
expect(text).not.toContain(String.raw`\n\tconst escaped`);
|
|
364
|
+
const json = jsonFromDiagnostic(text);
|
|
365
|
+
const logged = JSON.parse(json) as SystemOneRequest;
|
|
366
|
+
expect(logged.model).toBe(body.model);
|
|
367
|
+
expect(logged.state).toEqual(body.state);
|
|
368
|
+
expect(logged.state).toMatchObject({ ...readState, ...added });
|
|
369
|
+
if (minimum === "All") {
|
|
370
|
+
expect(json).toBe(JSON.stringify(body, null, 2));
|
|
371
|
+
expect(logged).toEqual(body);
|
|
372
|
+
} else {
|
|
373
|
+
expect(Object.keys(logged.state)[0]).toBe("code");
|
|
374
|
+
if (index === 0) {
|
|
375
|
+
expect(logged.questions).toEqual(body.questions);
|
|
376
|
+
expect(logged.questions.plain?.criteria).toBeDefined();
|
|
377
|
+
} else {
|
|
378
|
+
const { criteria, ...question } = body.questions["start:plain:1"]!;
|
|
379
|
+
expect(criteria).toEqual({ "1": lines[0] });
|
|
380
|
+
expect(logged.questions).toEqual({ "start:plain:1": question });
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
expect(
|
|
384
|
+
logs.find(({ message }) => Array.isArray(message) && message.includes(previews[index]))
|
|
385
|
+
?.logLevel,
|
|
386
|
+
).toBe("Debug");
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
expect(JSON.stringify(messages)).not.toContain("sk-saved");
|
|
390
|
+
expect(messages.some((message) => String(message).includes("too long request payload"))).toBe(
|
|
391
|
+
false,
|
|
392
|
+
);
|
|
393
|
+
},
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
it("is asked as a method, so a class can implement it", async () => {
|
|
397
|
+
class Fixed implements Provider {
|
|
398
|
+
readonly model = "fixed";
|
|
399
|
+
readonly #probability = 0.61;
|
|
400
|
+
async ask(_state: State, questions: ReadonlyArray<Question>, _options: AskOptions) {
|
|
401
|
+
return { answers: Object.fromEntries(questions.map(({ id }) => [id, this.#probability])) };
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const fixed = new Fixed();
|
|
405
|
+
const result = await Effect.runPromise(
|
|
406
|
+
Effect.flatMap(decodeConfig({ provider: fixed }), (config) =>
|
|
407
|
+
judged(fixed.ask, undefined, { plain }, config.provider),
|
|
408
|
+
),
|
|
409
|
+
);
|
|
410
|
+
expect(result.probabilities).toEqual({ plain: 0.61 });
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it("is asked again after a RequestFailed with no answer, a 408, a 429, or a 5xx, but refuses any other at once", async () => {
|
|
414
|
+
const statuses = [503, 200];
|
|
415
|
+
const retried = await Effect.runPromise(
|
|
416
|
+
judged(async () => {
|
|
417
|
+
const status = statuses.shift();
|
|
418
|
+
if (status !== 200) throw new RequestFailed(`Clef answered HTTP ${status}`, status ?? 0);
|
|
419
|
+
return { answers: { plain: 0.93 } };
|
|
420
|
+
}),
|
|
421
|
+
);
|
|
422
|
+
expect(retried.probabilities).toEqual({ plain: 0.93 });
|
|
423
|
+
expect(statuses).toEqual([]);
|
|
424
|
+
|
|
425
|
+
let asked = 0;
|
|
426
|
+
const refused = await Effect.runPromise(
|
|
427
|
+
Effect.flip(
|
|
428
|
+
judged(async () => {
|
|
429
|
+
asked++;
|
|
430
|
+
throw new RequestFailed("Clef answered HTTP 401: bad token", 401);
|
|
431
|
+
}),
|
|
432
|
+
),
|
|
433
|
+
);
|
|
434
|
+
expect(refused).toMatchObject({
|
|
435
|
+
_tag: "JevUnavailable",
|
|
436
|
+
message: "Clef answered HTTP 401: bad token",
|
|
437
|
+
});
|
|
438
|
+
expect(asked).toBe(1);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it("leaves a rule unanswered while any of its matchers is, so it is asked again", async () => {
|
|
442
|
+
const scoped: Rule = { ...plain, appliesTo: ["a server"], excludeIf: ["a test"] };
|
|
443
|
+
const answering = (answers: Readonly<Record<string, number>>) =>
|
|
444
|
+
Effect.runPromise(judged(async () => ({ answers }), undefined, { plain: scoped }));
|
|
445
|
+
const refusedMatcher = await answering({ plain: 0.93, "appliesTo:plain:0": 0.9 });
|
|
446
|
+
expect(refusedMatcher.probabilities).toEqual({});
|
|
447
|
+
expect(refusedMatcher.matchers).toBeUndefined();
|
|
448
|
+
const all = await answering({
|
|
449
|
+
plain: 0.93,
|
|
450
|
+
"appliesTo:plain:0": 0.9,
|
|
451
|
+
"excludeIf:plain:0": 0.1,
|
|
452
|
+
});
|
|
453
|
+
expect(all.probabilities).toEqual({ plain: 0.93 });
|
|
454
|
+
expect(all.matchers).toEqual({ plain: { appliesTo: [0.9], excludeIf: [0.1] } });
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("refuses the run with what it said when it rejects otherwise", async () => {
|
|
458
|
+
const refused = await Effect.runPromise(
|
|
459
|
+
Effect.flip(judged(() => Promise.reject(new Error("Clef answered HTTP 429: slow down")))),
|
|
460
|
+
);
|
|
461
|
+
expect(refused).toMatchObject({
|
|
462
|
+
_tag: "JevUnavailable",
|
|
463
|
+
message: "Clef answered HTTP 429: slow down",
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it("refuses the run when its answers are not probabilities and keys", async () => {
|
|
468
|
+
const refused = await Effect.runPromise(
|
|
469
|
+
Effect.flip(judged(async () => ({ answers: { plain: { noul: 0.93 } } }) as never)),
|
|
470
|
+
);
|
|
471
|
+
expect(refused).toMatchObject({
|
|
472
|
+
_tag: "JevUnavailable",
|
|
473
|
+
message: expect.stringContaining("clef-flash's answers did not decode"),
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it("is a model and a function", async () => {
|
|
478
|
+
const refused = await Effect.runPromise(
|
|
479
|
+
Effect.flip(decodeConfig({ provider: { model: "clef-flash", ask: "https://example.com" } })),
|
|
480
|
+
);
|
|
481
|
+
expect(refused.message).toContain("a function");
|
|
482
|
+
});
|
|
483
|
+
});
|