@restormel/testing-runner 0.1.1 → 0.1.5

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.
@@ -0,0 +1,21 @@
1
+ import type { Page } from "playwright";
2
+ import type { AcceptanceCriterionDefinition } from "@restormel/testing-core";
3
+ import type { ResolvedModel } from "@restormel/testing-keys-adapter";
4
+ export type AcAgentLoopResult = {
5
+ ok: true;
6
+ roundsUsed: number;
7
+ finished: "done" | "max_rounds";
8
+ } | {
9
+ ok: false;
10
+ roundsUsed: number;
11
+ reasonCode: string;
12
+ summary: string;
13
+ };
14
+ /**
15
+ * Multi-turn tool-use style loop: model proposes JSON actions until `done` / `give_up` / max rounds.
16
+ */
17
+ export declare function runBuiltInAcAgentLoop(page: Page, ac: AcceptanceCriterionDefinition, model: ResolvedModel, baseUrl: string, options?: {
18
+ maxRounds?: number;
19
+ instructions?: string;
20
+ }): Promise<AcAgentLoopResult>;
21
+ //# sourceMappingURL=ac-agent-loop.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ac-agent-loop.d.ts","sourceRoot":"","sources":["../src/ac-agent-loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,yBAAyB,CAAC;AAC7E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAyGrE,MAAM,MAAM,iBAAiB,GACzB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,CAAA;CAAE,GACjE;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3E;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,IAAI,EACV,EAAE,EAAE,6BAA6B,EACjC,KAAK,EAAE,aAAa,EACpB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,GACtD,OAAO,CAAC,iBAAiB,CAAC,CAgF5B"}
@@ -0,0 +1,174 @@
1
+ import { postChatCompletions } from "./ac-llm.js";
2
+ function truncate(s, n) {
3
+ return s.length <= n ? s : `${s.slice(0, n)}…`;
4
+ }
5
+ async function pageSnippet(page) {
6
+ const url = page.url();
7
+ const title = await page.title().catch(() => "");
8
+ const body = await page.locator("body").innerText().catch(() => "");
9
+ return `URL: ${url}\nTitle: ${title}\nBody (truncated):\n${truncate(body.trim(), 6000)}`;
10
+ }
11
+ function resolveNavUrl(href, baseUrl) {
12
+ try {
13
+ const b = new URL(baseUrl);
14
+ const t = new URL(href.trim(), b);
15
+ if (t.origin !== b.origin)
16
+ return null;
17
+ return t.href;
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ function parseAgentAction(raw) {
24
+ let o;
25
+ try {
26
+ o = JSON.parse(raw);
27
+ }
28
+ catch {
29
+ return undefined;
30
+ }
31
+ if (o === null || typeof o !== "object")
32
+ return undefined;
33
+ const a = o;
34
+ const action = typeof a.action === "string" ? a.action.toLowerCase() : "";
35
+ if (action === "navigate" && typeof a.url === "string")
36
+ return { action: "navigate", url: a.url };
37
+ if (action === "click_css" && typeof a.selector === "string")
38
+ return { action: "click_css", selector: a.selector };
39
+ if (action === "click_role" && typeof a.role === "string")
40
+ return { action: "click_role", role: a.role, name: typeof a.name === "string" ? a.name : undefined };
41
+ if (action === "fill" && typeof a.role === "string" && typeof a.value === "string")
42
+ return {
43
+ action: "fill",
44
+ role: a.role,
45
+ name: typeof a.name === "string" ? a.name : undefined,
46
+ value: a.value,
47
+ };
48
+ if (action === "wait_load")
49
+ return {
50
+ action: "wait_load",
51
+ state: a.state === "domcontentloaded" || a.state === "networkidle" || a.state === "load"
52
+ ? a.state
53
+ : undefined,
54
+ };
55
+ if (action === "done")
56
+ return { action: "done" };
57
+ if (action === "give_up")
58
+ return { action: "give_up", reason: typeof a.reason === "string" ? a.reason : undefined };
59
+ return undefined;
60
+ }
61
+ async function executeAction(page, act, baseUrl) {
62
+ const timeout = 15_000;
63
+ try {
64
+ if (act.action === "navigate") {
65
+ const u = resolveNavUrl(act.url, baseUrl) ?? resolveNavUrl(act.url, page.url());
66
+ if (!u)
67
+ return { ok: false, err: "navigate: URL not allowed (same-origin only)" };
68
+ await page.goto(u, { waitUntil: "load", timeout });
69
+ return { ok: true };
70
+ }
71
+ if (act.action === "click_css") {
72
+ await page.locator(act.selector).first().click({ timeout });
73
+ return { ok: true };
74
+ }
75
+ if (act.action === "click_role") {
76
+ const loc = page.getByRole(act.role, act.name ? { name: new RegExp(escapeReg(act.name), "i") } : undefined);
77
+ await loc.first().click({ timeout });
78
+ return { ok: true };
79
+ }
80
+ if (act.action === "fill") {
81
+ const loc = page.getByRole(act.role, act.name ? { name: new RegExp(escapeReg(act.name), "i") } : undefined);
82
+ await loc.first().fill(act.value, { timeout });
83
+ return { ok: true };
84
+ }
85
+ if (act.action === "wait_load") {
86
+ await page.waitForLoadState(act.state ?? "networkidle", { timeout });
87
+ return { ok: true };
88
+ }
89
+ return { ok: false, err: "unhandled action" };
90
+ }
91
+ catch (e) {
92
+ return { ok: false, err: e instanceof Error ? e.message : String(e) };
93
+ }
94
+ }
95
+ function escapeReg(s) {
96
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
97
+ }
98
+ /**
99
+ * Multi-turn tool-use style loop: model proposes JSON actions until `done` / `give_up` / max rounds.
100
+ */
101
+ export async function runBuiltInAcAgentLoop(page, ac, model, baseUrl, options) {
102
+ const maxRounds = options?.maxRounds ?? 12;
103
+ const extra = options?.instructions?.trim() ? `\n${options.instructions.trim()}` : "";
104
+ const system = `You are a browser automation agent. You must satisfy ONE acceptance criterion at a time using the page.
105
+ Output a single JSON object per message (no markdown). Allowed actions:
106
+ - {"action":"navigate","url":"<path or absolute same-origin URL>"}
107
+ - {"action":"click_css","selector":"<CSS selector>"}
108
+ - {"action":"click_role","role":"<aria role>","name":"<optional accessible name substring>"}
109
+ - {"action":"fill","role":"textbox","name":"<optional>","value":"<text>"}
110
+ - {"action":"wait_load","state":"networkidle"|"load"|"domcontentloaded"}
111
+ - {"action":"done"} when the criterion is satisfied on the current page
112
+ - {"action":"give_up","reason":"..."} if blocked
113
+
114
+ Rules: stay on the same origin as the starting base URL. Prefer role-based actions over fragile CSS. Never output secrets.${extra}`;
115
+ const messages = [
116
+ { role: "system", content: system },
117
+ {
118
+ role: "user",
119
+ content: `Criterion id: ${ac.id}\nCriterion: ${ac.text}\n\n${await pageSnippet(page)}`,
120
+ },
121
+ ];
122
+ let roundsUsed = 0;
123
+ for (let i = 0; i < maxRounds; i++) {
124
+ roundsUsed++;
125
+ const chat = await postChatCompletions(model, messages, { maxTokens: 400, temperature: 0 });
126
+ if (!chat.ok) {
127
+ return {
128
+ ok: false,
129
+ roundsUsed,
130
+ reasonCode: "AC_AGENT_LLM_ERROR",
131
+ summary: chat.summary,
132
+ };
133
+ }
134
+ messages.push({ role: "assistant", content: chat.content });
135
+ const act = parseAgentAction(chat.content);
136
+ if (!act) {
137
+ messages.push({
138
+ role: "user",
139
+ content: "Invalid JSON or unknown action. Reply with one JSON object only.",
140
+ });
141
+ continue;
142
+ }
143
+ if (act.action === "done") {
144
+ return { ok: true, roundsUsed, finished: "done" };
145
+ }
146
+ if (act.action === "give_up") {
147
+ return {
148
+ ok: false,
149
+ roundsUsed,
150
+ reasonCode: "AC_AGENT_GAVE_UP",
151
+ summary: act.reason ?? "Agent gave up",
152
+ };
153
+ }
154
+ const ex = await executeAction(page, act, baseUrl);
155
+ if (!ex.ok) {
156
+ messages.push({
157
+ role: "user",
158
+ content: `Action failed: ${ex.err}\n${await pageSnippet(page)}`,
159
+ });
160
+ continue;
161
+ }
162
+ messages.push({
163
+ role: "user",
164
+ content: `Action ok.\n${await pageSnippet(page)}`,
165
+ });
166
+ }
167
+ return {
168
+ ok: false,
169
+ roundsUsed,
170
+ reasonCode: "AC_AGENT_MAX_ROUNDS",
171
+ summary: `Exceeded ${maxRounds} rounds without done`,
172
+ };
173
+ }
174
+ //# sourceMappingURL=ac-agent-loop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ac-agent-loop.js","sourceRoot":"","sources":["../src/ac-agent-loop.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { Page } from "playwright";
2
+ import type { CriteriaEvaluation } from "./evaluate-criteria.js";
3
+ import type { JudgeRubric } from "@restormel/testing-core";
4
+ import type { ResolvedModel } from "@restormel/testing-keys-adapter";
5
+ /**
6
+ * Per-AC judge: response JSON must include matching `ac_id` (R-BA-5).
7
+ */
8
+ export declare function runAcShapedJudgeRubric(page: Page, rubric: JudgeRubric, model: ResolvedModel, ac: {
9
+ id: string;
10
+ text: string;
11
+ }): Promise<CriteriaEvaluation>;
12
+ //# sourceMappingURL=ac-judge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ac-judge.d.ts","sourceRoot":"","sources":["../src/ac-judge.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAmBrE;;GAEG;AACH,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,aAAa,EACpB,EAAE,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC/B,OAAO,CAAC,kBAAkB,CAAC,CAqG7B"}
@@ -0,0 +1,116 @@
1
+ function truncate(s, n) {
2
+ return s.length <= n ? s : `${s.slice(0, n)}…`;
3
+ }
4
+ const MAX_SAMPLE = 8000;
5
+ async function sampleTextForAcJudge(page, rubric) {
6
+ if (rubric.contextSelector !== undefined && rubric.contextSelector.trim() !== "") {
7
+ const t = await page.locator(rubric.contextSelector).first().innerText().catch(() => "");
8
+ return truncate(t.trim(), MAX_SAMPLE);
9
+ }
10
+ const mainText = await page.locator("main").first().innerText().catch(() => "");
11
+ if (mainText.trim().length > 0)
12
+ return truncate(mainText.trim(), MAX_SAMPLE);
13
+ const bodyText = await page.locator("body").innerText().catch(() => "");
14
+ return truncate(bodyText.trim(), MAX_SAMPLE);
15
+ }
16
+ /**
17
+ * Per-AC judge: response JSON must include matching `ac_id` (R-BA-5).
18
+ */
19
+ export async function runAcShapedJudgeRubric(page, rubric, model, ac) {
20
+ const sample = await sampleTextForAcJudge(page, rubric);
21
+ const base = model.providerBaseUrl?.replace(/\/?$/, "") ?? "https://api.openai.com/v1";
22
+ const url = `${base}/chat/completions`;
23
+ const system = `You are a test oracle for one acceptance criterion. Reply with a single JSON object only:
24
+ {"verdict":"pass"|"fail"|"uncertain","ac_id":"<string>","reason":"<short>"}
25
+ The ac_id field MUST exactly equal the acceptance criterion id provided in the user message.`;
26
+ const user = `Acceptance criterion id: ${ac.id}
27
+ Criterion text: ${ac.text}
28
+
29
+ Rubric id: ${rubric.id}
30
+ Rubric summary: ${rubric.summary ?? "(none)"}
31
+
32
+ Page text:
33
+ ${sample}`;
34
+ try {
35
+ const res = await fetch(url, {
36
+ method: "POST",
37
+ headers: {
38
+ "content-type": "application/json",
39
+ authorization: `Bearer ${model.credentials.apiKey}`,
40
+ },
41
+ body: JSON.stringify({
42
+ model: model.modelId,
43
+ messages: [
44
+ { role: "system", content: system },
45
+ { role: "user", content: user },
46
+ ],
47
+ max_tokens: 120,
48
+ temperature: 0,
49
+ response_format: { type: "json_object" },
50
+ }),
51
+ });
52
+ if (!res.ok) {
53
+ const t = await res.text().catch(() => "");
54
+ return {
55
+ verdict: "indeterminate",
56
+ reasonCode: "JUDGE_HTTP_ERROR",
57
+ summary: `AC judge HTTP ${res.status}: ${truncate(t, 80)}`,
58
+ judgeModelInvocations: 1,
59
+ };
60
+ }
61
+ const data = (await res.json());
62
+ const raw = data.choices?.[0]?.message?.content ?? "{}";
63
+ let parsed;
64
+ try {
65
+ parsed = JSON.parse(raw);
66
+ }
67
+ catch {
68
+ return {
69
+ verdict: "indeterminate",
70
+ reasonCode: "JUDGE_PARSE_ERROR",
71
+ summary: "AC judge response was not valid JSON",
72
+ judgeModelInvocations: 1,
73
+ };
74
+ }
75
+ if (parsed.ac_id !== ac.id) {
76
+ return {
77
+ verdict: "indeterminate",
78
+ reasonCode: "JUDGE_AC_ID_MISMATCH",
79
+ summary: `Model ac_id ${JSON.stringify(parsed.ac_id)} did not match expected ${JSON.stringify(ac.id)}`,
80
+ judgeModelInvocations: 1,
81
+ };
82
+ }
83
+ const v = (parsed.verdict ?? "").toLowerCase();
84
+ const reason = parsed.reason ? `: ${parsed.reason}` : "";
85
+ if (v === "pass") {
86
+ return {
87
+ verdict: "passed",
88
+ reasonCode: "JUDGE_AC_PASS",
89
+ summary: `AC judge passed${reason}`,
90
+ judgeModelInvocations: 1,
91
+ };
92
+ }
93
+ if (v === "fail") {
94
+ return {
95
+ verdict: "failed",
96
+ reasonCode: "JUDGE_AC_FAIL",
97
+ summary: `AC judge failed${reason}`,
98
+ judgeModelInvocations: 1,
99
+ };
100
+ }
101
+ return {
102
+ verdict: "indeterminate",
103
+ reasonCode: "JUDGE_AC_UNCERTAIN",
104
+ summary: `AC judge uncertain${reason}`,
105
+ judgeModelInvocations: 1,
106
+ };
107
+ }
108
+ catch (e) {
109
+ return {
110
+ verdict: "indeterminate",
111
+ reasonCode: "JUDGE_ERROR",
112
+ summary: `AC judge error: ${e instanceof Error ? e.message : String(e)}`,
113
+ };
114
+ }
115
+ }
116
+ //# sourceMappingURL=ac-judge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ac-judge.js","sourceRoot":"","sources":["../src/ac-judge.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import type { ResolvedModel } from "@restormel/testing-keys-adapter";
2
+ export type ChatMessage = {
3
+ role: "system" | "user" | "assistant";
4
+ content: string;
5
+ };
6
+ /**
7
+ * OpenAI-compatible chat/completions call (same transport as judge rubric).
8
+ */
9
+ export declare function postChatCompletions(model: ResolvedModel, messages: ChatMessage[], options?: {
10
+ maxTokens?: number;
11
+ temperature?: number;
12
+ }): Promise<{
13
+ ok: true;
14
+ content: string;
15
+ } | {
16
+ ok: false;
17
+ summary: string;
18
+ }>;
19
+ //# sourceMappingURL=ac-llm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ac-llm.d.ts","sourceRoot":"","sources":["../src/ac-llm.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAErE,MAAM,MAAM,WAAW,GAAG;IAAE,IAAI,EAAE,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAMrF;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,aAAa,EACpB,QAAQ,EAAE,WAAW,EAAE,EACvB,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GACrD,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAyCzE"}
package/dist/ac-llm.js ADDED
@@ -0,0 +1,47 @@
1
+ function redactForLog(s) {
2
+ return s.replace(/\bBearer\s+[\w-_.]+\b/gi, "Bearer [redacted]").replace(/\bsk-[a-zA-Z0-9]{10,}\b/g, "sk-[redacted]");
3
+ }
4
+ /**
5
+ * OpenAI-compatible chat/completions call (same transport as judge rubric).
6
+ */
7
+ export async function postChatCompletions(model, messages, options) {
8
+ const base = model.providerBaseUrl?.replace(/\/?$/, "") ?? "https://api.openai.com/v1";
9
+ const url = `${base}/chat/completions`;
10
+ const maxTokens = options?.maxTokens ?? 400;
11
+ const temperature = options?.temperature ?? 0;
12
+ try {
13
+ const res = await fetch(url, {
14
+ method: "POST",
15
+ headers: {
16
+ "content-type": "application/json",
17
+ authorization: `Bearer ${model.credentials.apiKey}`,
18
+ },
19
+ body: JSON.stringify({
20
+ model: model.modelId,
21
+ messages,
22
+ max_tokens: maxTokens,
23
+ temperature,
24
+ }),
25
+ });
26
+ if (!res.ok) {
27
+ const t = await res.text().catch(() => "");
28
+ return {
29
+ ok: false,
30
+ summary: `LLM HTTP ${res.status} ${redactForLog(t).slice(0, 120)}`,
31
+ };
32
+ }
33
+ const data = (await res.json());
34
+ const content = data.choices?.[0]?.message?.content?.trim() ?? "";
35
+ if (!content) {
36
+ return { ok: false, summary: "Empty model response" };
37
+ }
38
+ return { ok: true, content };
39
+ }
40
+ catch (e) {
41
+ return {
42
+ ok: false,
43
+ summary: `LLM error: ${e instanceof Error ? e.message : String(e)}`,
44
+ };
45
+ }
46
+ }
47
+ //# sourceMappingURL=ac-llm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ac-llm.js","sourceRoot":"","sources":["../src/ac-llm.ts"],"names":[],"mappings":"AAIA,SAAS,YAAY,CAAC,CAAS;IAC7B,OAAO,CAAC,CAAC,OAAO,CAAC,yBAAyB,EAAE,mBAAmB,CAAC,CAAC,OAAO,CAAC,0BAA0B,EAAE,eAAe,CAAC,CAAC;AACxH,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,KAAoB,EACpB,QAAuB,EACvB,OAAsD;IAEtD,MAAM,IAAI,GAAG,KAAK,CAAC,eAAe,EAAE,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,2BAA2B,CAAC;IACvF,MAAM,GAAG,GAAG,GAAG,IAAI,mBAAmB,CAAC;IACvC,MAAM,SAAS,GAAG,OAAO,EAAE,SAAS,IAAI,GAAG,CAAC;IAC5C,MAAM,WAAW,GAAG,OAAO,EAAE,WAAW,IAAI,CAAC,CAAC;IAE9C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YAC3B,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,aAAa,EAAE,UAAU,KAAK,CAAC,WAAW,CAAC,MAAM,EAAE;aACpD;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,KAAK,EAAE,KAAK,CAAC,OAAO;gBACpB,QAAQ;gBACR,UAAU,EAAE,SAAS;gBACrB,WAAW;aACZ,CAAC;SACH,CAAC,CAAC;QAEH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,CAAC,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;YAC3C,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,OAAO,EAAE,YAAY,GAAG,CAAC,MAAM,IAAI,YAAY,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE;aACnE,CAAC;QACJ,CAAC;QAED,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAA4D,CAAC;QAC3F,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;QAClE,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,sBAAsB,EAAE,CAAC;QACxD,CAAC;QACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;IAC/B,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO;YACL,EAAE,EAAE,KAAK;YACT,OAAO,EAAE,cAAc,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE;SACpE,CAAC;IACJ,CAAC;AACH,CAAC"}
@@ -0,0 +1,18 @@
1
+ import type { Page } from "playwright";
2
+ import type { AcSequencePostCheck } from "@restormel/testing-core";
3
+ export type PostCheckResult = {
4
+ ok: true;
5
+ } | {
6
+ ok: false;
7
+ reasonCode: string;
8
+ summary: string;
9
+ };
10
+ /**
11
+ * R-BA-6 style checks after agent work: HTTP, DOM role+name, optional db shell.
12
+ */
13
+ export declare function runAcPostCheck(page: Page, check: AcSequencePostCheck, ctx: {
14
+ baseUrl: string;
15
+ hookCwd: string;
16
+ extraEnv: Record<string, string | undefined>;
17
+ }): Promise<PostCheckResult>;
18
+ //# sourceMappingURL=ac-post-checks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ac-post-checks.d.ts","sourceRoot":"","sources":["../src/ac-post-checks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAInE,MAAM,MAAM,eAAe,GACvB;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAYvD;;GAEG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,mBAAmB,EAC1B,GAAG,EAAE;IACH,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;CAC9C,GACA,OAAO,CAAC,eAAe,CAAC,CAoF1B"}
@@ -0,0 +1,101 @@
1
+ import { isSafeHttpUrl } from "@restormel/testing-config";
2
+ import { runMissionExecutorCommand } from "./shell-hooks.js";
3
+ function resolveCheckUrl(raw, baseUrl) {
4
+ const t = raw.trim();
5
+ if (t === "")
6
+ return undefined;
7
+ try {
8
+ return new URL(t, baseUrl).href;
9
+ }
10
+ catch {
11
+ return undefined;
12
+ }
13
+ }
14
+ /**
15
+ * R-BA-6 style checks after agent work: HTTP, DOM role+name, optional db shell.
16
+ */
17
+ export async function runAcPostCheck(page, check, ctx) {
18
+ if (check.http !== undefined) {
19
+ const href = resolveCheckUrl(check.http.url, ctx.baseUrl);
20
+ if (!href) {
21
+ return { ok: false, reasonCode: "POST_CHECK_HTTP_BAD_URL", summary: "Invalid post_check http url" };
22
+ }
23
+ const urlCheck = isSafeHttpUrl(href);
24
+ if (!urlCheck.ok) {
25
+ return { ok: false, reasonCode: "POST_CHECK_HTTP_UNSAFE", summary: urlCheck.reason };
26
+ }
27
+ const method = (check.http.method ?? "GET").toUpperCase();
28
+ const expectStatuses = check.http.expectStatus?.length ? check.http.expectStatus : [200];
29
+ try {
30
+ const res = await fetch(href, {
31
+ method,
32
+ headers: check.http.headers,
33
+ body: method === "GET" || method === "HEAD" ? undefined : check.http.body,
34
+ });
35
+ if (!expectStatuses.includes(res.status)) {
36
+ return {
37
+ ok: false,
38
+ reasonCode: "POST_CHECK_HTTP_STATUS",
39
+ summary: `Expected status ${expectStatuses.join("|")}, got ${res.status} for ${href}`,
40
+ };
41
+ }
42
+ }
43
+ catch (e) {
44
+ return {
45
+ ok: false,
46
+ reasonCode: "POST_CHECK_HTTP_ERROR",
47
+ summary: `HTTP check failed: ${e instanceof Error ? e.message : String(e)}`,
48
+ };
49
+ }
50
+ }
51
+ if (check.domRoleName !== undefined) {
52
+ const { role, name, expectVisible = true } = check.domRoleName;
53
+ const r = role.trim();
54
+ if (r === "") {
55
+ return { ok: false, reasonCode: "POST_CHECK_DOM_ROLE_EMPTY", summary: "dom_role_name.role is empty" };
56
+ }
57
+ try {
58
+ const loc = page.getByRole(r, name ? { name: new RegExp(escapeRegExp(name), "i") } : undefined);
59
+ const n = await loc.count();
60
+ if (n === 0) {
61
+ return {
62
+ ok: false,
63
+ reasonCode: "POST_CHECK_DOM_ROLE_MISSING",
64
+ summary: `No element role=${r}${name ? ` name~${JSON.stringify(name)}` : ""}`,
65
+ };
66
+ }
67
+ if (expectVisible) {
68
+ const vis = await loc.first().isVisible().catch(() => false);
69
+ if (!vis) {
70
+ return {
71
+ ok: false,
72
+ reasonCode: "POST_CHECK_DOM_ROLE_HIDDEN",
73
+ summary: `role=${r} exists but not visible`,
74
+ };
75
+ }
76
+ }
77
+ }
78
+ catch (e) {
79
+ return {
80
+ ok: false,
81
+ reasonCode: "POST_CHECK_DOM_ERROR",
82
+ summary: e instanceof Error ? e.message : String(e),
83
+ };
84
+ }
85
+ }
86
+ if (check.dbShell !== undefined && check.dbShell.trim() !== "") {
87
+ const r = await runMissionExecutorCommand(check.dbShell.trim(), {
88
+ cwd: ctx.hookCwd,
89
+ label: `post_check db_shell (${check.acId})`,
90
+ extraEnv: ctx.extraEnv,
91
+ });
92
+ if (!r.ok) {
93
+ return { ok: false, reasonCode: "POST_CHECK_DB_SHELL_FAILED", summary: r.message };
94
+ }
95
+ }
96
+ return { ok: true };
97
+ }
98
+ function escapeRegExp(s) {
99
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
100
+ }
101
+ //# sourceMappingURL=ac-post-checks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ac-post-checks.js","sourceRoot":"","sources":["../src/ac-post-checks.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -3,6 +3,8 @@
3
3
  */
4
4
  export { runBrowserGoal } from "./browser-goal.js";
5
5
  export type { RunBrowserGoalOptions, RunBrowserGoalResult } from "./browser-goal.js";
6
+ export { runAcSequenceBrowserGoal } from "./run-ac-sequence-goal.js";
7
+ export type { RunAcSequenceBrowserGoalOptions } from "./run-ac-sequence-goal.js";
6
8
  export { evaluateBrowserSuccessCriteria } from "./evaluate-criteria.js";
7
9
  export type { CriteriaEvaluation } from "./evaluate-criteria.js";
8
10
  export { runGoalAttempts } from "./retries.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,YAAY,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACrF,OAAO,EAAE,8BAA8B,EAAE,MAAM,wBAAwB,CAAC;AACxE,YAAY,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACnE,YAAY,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjG,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,YAAY,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACrF,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AACrE,YAAY,EAAE,+BAA+B,EAAE,MAAM,2BAA2B,CAAC;AACjF,OAAO,EAAE,8BAA8B,EAAE,MAAM,wBAAwB,CAAC;AACxE,YAAY,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACnE,YAAY,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjG,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC"}
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  * @restormel/testing-runner — local suite execution (MVP).
3
3
  */
4
4
  export { runBrowserGoal } from "./browser-goal.js";
5
+ export { runAcSequenceBrowserGoal } from "./run-ac-sequence-goal.js";
5
6
  export { evaluateBrowserSuccessCriteria } from "./evaluate-criteria.js";
6
7
  export { runGoalAttempts } from "./retries.js";
7
8
  export { runLocalSuite, runSuiteFromConfig } from "./run-suite.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD,OAAO,EAAE,8BAA8B,EAAE,MAAM,wBAAwB,CAAC;AAExE,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAEnE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAErE,OAAO,EAAE,8BAA8B,EAAE,MAAM,wBAAwB,CAAC;AAExE,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAEnE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { TestGoal } from "@restormel/testing-core";
2
+ /**
3
+ * After `mission_executor` exits 0, browser evaluation uses this goal: agent-only fields removed,
4
+ * navigation path is `after_agent.start_path` when set, otherwise the goal's top-level `start_path`.
5
+ */
6
+ export declare function toPostMissionObserveGoal(goal: TestGoal): TestGoal;
7
+ //# sourceMappingURL=post-mission-goal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"post-mission-goal.d.ts","sourceRoot":"","sources":["../src/post-mission-goal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAExD;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,QAAQ,GAAG,QAAQ,CAiBjE"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * After `mission_executor` exits 0, browser evaluation uses this goal: agent-only fields removed,
3
+ * navigation path is `after_agent.start_path` when set, otherwise the goal's top-level `start_path`.
4
+ */
5
+ export function toPostMissionObserveGoal(goal) {
6
+ const startPath = goal.afterAgent?.startPath ?? goal.startPath;
7
+ return {
8
+ id: goal.id,
9
+ type: goal.type,
10
+ description: goal.description,
11
+ successCriteria: goal.successCriteria,
12
+ startPath,
13
+ executionMode: "observe",
14
+ preconditions: goal.preconditions,
15
+ cleanup: goal.cleanup,
16
+ exclusiveWith: goal.exclusiveWith,
17
+ tags: goal.tags,
18
+ ...(goal.acceptanceCriterionIds !== undefined && goal.acceptanceCriterionIds.length > 0
19
+ ? { acceptanceCriterionIds: [...goal.acceptanceCriterionIds] }
20
+ : {}),
21
+ };
22
+ }
23
+ //# sourceMappingURL=post-mission-goal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"post-mission-goal.js","sourceRoot":"","sources":["../src/post-mission-goal.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,UAAU,wBAAwB,CAAC,IAAc;IACrD,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,EAAE,SAAS,IAAI,IAAI,CAAC,SAAS,CAAC;IAC/D,OAAO;QACL,EAAE,EAAE,IAAI,CAAC,EAAE;QACX,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,eAAe,EAAE,IAAI,CAAC,eAAe;QACrC,SAAS;QACT,aAAa,EAAE,SAAS;QACxB,aAAa,EAAE,IAAI,CAAC,aAAa;QACjC,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,aAAa,EAAE,IAAI,CAAC,aAAa;QACjC,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,GAAG,CAAC,IAAI,CAAC,sBAAsB,KAAK,SAAS,IAAI,IAAI,CAAC,sBAAsB,CAAC,MAAM,GAAG,CAAC;YACrF,CAAC,CAAC,EAAE,sBAAsB,EAAE,CAAC,GAAG,IAAI,CAAC,sBAAsB,CAAC,EAAE;YAC9D,CAAC,CAAC,EAAE,CAAC;KACR,CAAC;AACJ,CAAC"}