@davidrydberg/proofy 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +64 -2
- package/dist/args.d.ts +10 -0
- package/dist/args.js +36 -0
- package/dist/bin/proofy.d.ts +2 -0
- package/dist/bin/proofy.js +4 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.js +140 -0
- package/dist/commands/approve.d.ts +4 -0
- package/dist/commands/approve.js +48 -0
- package/dist/commands/context.d.ts +16 -0
- package/dist/commands/context.js +27 -0
- package/dist/commands/dev.d.ts +4 -0
- package/dist/commands/dev.js +161 -0
- package/dist/commands/home.d.ts +3 -0
- package/dist/commands/home.js +122 -0
- package/dist/commands/init.d.ts +4 -0
- package/dist/commands/init.js +122 -0
- package/dist/commands/report.d.ts +4 -0
- package/dist/commands/report.js +26 -0
- package/dist/commands/run.d.ts +6 -0
- package/dist/commands/run.js +166 -0
- package/dist/commands/types.d.ts +8 -0
- package/dist/commands/types.js +1 -0
- package/dist/config.d.ts +34 -0
- package/dist/config.js +62 -0
- package/dist/deps.d.ts +10 -0
- package/dist/deps.js +1 -0
- package/dist/detect.d.ts +17 -0
- package/dist/detect.js +96 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +21 -0
- package/dist/github.d.ts +16 -0
- package/dist/github.js +121 -0
- package/dist/human.d.ts +30 -0
- package/dist/human.js +160 -0
- package/dist/output.d.ts +8 -0
- package/dist/output.js +21 -0
- package/dist/project.d.ts +21 -0
- package/dist/project.js +71 -0
- package/dist/run/approvals.d.ts +15 -0
- package/dist/run/approvals.js +26 -0
- package/dist/run/engine.d.ts +48 -0
- package/dist/run/engine.js +168 -0
- package/dist/run/fingerprint.d.ts +11 -0
- package/dist/run/fingerprint.js +40 -0
- package/dist/run/history.d.ts +29 -0
- package/dist/run/history.js +85 -0
- package/dist/run/report.d.ts +14 -0
- package/dist/run/report.js +115 -0
- package/dist/run/results.d.ts +70 -0
- package/dist/run/results.js +129 -0
- package/dist/run/spec.d.ts +6 -0
- package/dist/run/spec.js +16 -0
- package/dist/sandbox/compose.d.ts +38 -0
- package/dist/sandbox/compose.js +132 -0
- package/dist/sandbox/contract.d.ts +19 -0
- package/dist/sandbox/contract.js +20 -0
- package/dist/sandbox/docker.d.ts +17 -0
- package/dist/sandbox/docker.js +48 -0
- package/dist/sandbox/sandbox.d.ts +46 -0
- package/dist/sandbox/sandbox.js +239 -0
- package/dist/standin/ca.d.ts +6 -0
- package/dist/standin/ca.js +40 -0
- package/dist/standin/dns.d.ts +10 -0
- package/dist/standin/dns.js +53 -0
- package/dist/standin/forward.d.ts +1 -0
- package/dist/standin/forward.js +17 -0
- package/dist/standin/handle.d.ts +49 -0
- package/dist/standin/handle.js +130 -0
- package/dist/standin/server.d.ts +1 -0
- package/dist/standin/server.js +88 -0
- package/dist/style.d.ts +23 -0
- package/dist/style.js +38 -0
- package/dist/test/global-setup.d.ts +1 -0
- package/dist/test/global-setup.js +20 -0
- package/dist/test/index.d.ts +102 -0
- package/dist/test/index.js +184 -0
- package/docker/standin.Dockerfile +3 -0
- package/package.json +68 -4
- package/schema/proofy.schema.json +179 -0
- package/templates/e2e/AGENTS.md +90 -0
- package/templates/e2e/templates/compose.yml +33 -0
- package/templates/e2e/templates/spec.md +20 -0
- package/templates/e2e/templates/standin.mjs +29 -0
- package/templates/e2e/templates/test.spec.ts +28 -0
package/dist/style.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Kept ASCII in source; these render as check mark, cross and bullet.
|
|
2
|
+
export const GLYPH = { ok: "\u2713", fail: "\u2717", warn: "!", dot: "\u2022" };
|
|
3
|
+
export function resolveStyle(env, isTTY, columns) {
|
|
4
|
+
let color = isTTY;
|
|
5
|
+
if (env.FORCE_COLOR !== undefined) {
|
|
6
|
+
color = env.FORCE_COLOR !== "0" && env.FORCE_COLOR !== "false";
|
|
7
|
+
}
|
|
8
|
+
if (env.NO_COLOR) {
|
|
9
|
+
color = false;
|
|
10
|
+
}
|
|
11
|
+
return { color, width: Math.max(60, columns || 100) };
|
|
12
|
+
}
|
|
13
|
+
export function painter(style) {
|
|
14
|
+
const wrap = (open, close) => (text) => style.color && text ? `\u001b[${open}m${text}\u001b[${close}m` : text;
|
|
15
|
+
return {
|
|
16
|
+
bold: wrap(1, 22),
|
|
17
|
+
dim: wrap(2, 22),
|
|
18
|
+
red: wrap(31, 39),
|
|
19
|
+
green: wrap(32, 39),
|
|
20
|
+
yellow: wrap(33, 39),
|
|
21
|
+
cyan: wrap(36, 39),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function truncate(text, max) {
|
|
25
|
+
if (max <= 3)
|
|
26
|
+
return text.slice(0, Math.max(0, max));
|
|
27
|
+
return text.length > max ? `${text.slice(0, max - 3)}...` : text;
|
|
28
|
+
}
|
|
29
|
+
// Pads by visible length so colored text still lines up.
|
|
30
|
+
export function padVisible(text, width, align = "left") {
|
|
31
|
+
const visible = stripAnsi(text).length;
|
|
32
|
+
const pad = " ".repeat(Math.max(0, width - visible));
|
|
33
|
+
return align === "left" ? text + pad : pad + text;
|
|
34
|
+
}
|
|
35
|
+
const ANSI = new RegExp(`${String.fromCharCode(27)}\\[\\d+m`, "g");
|
|
36
|
+
export function stripAnsi(text) {
|
|
37
|
+
return text.replace(ANSI, "");
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function globalSetup(): Promise<void>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Runs once in the runner container before any test: waits until the app answers, so a slow boot is reported
|
|
2
|
+
// as an environment problem instead of as failed requirements.
|
|
3
|
+
export default async function globalSetup() {
|
|
4
|
+
const url = process.env.PROOFY_BASE_URL ?? "";
|
|
5
|
+
const deadline = Date.now() + Number(process.env.PROOFY_START_MS ?? 600_000);
|
|
6
|
+
let last = "no answer";
|
|
7
|
+
while (Date.now() < deadline) {
|
|
8
|
+
try {
|
|
9
|
+
const response = await fetch(url, { redirect: "manual", signal: AbortSignal.timeout(5000) });
|
|
10
|
+
if (response.status < 500)
|
|
11
|
+
return;
|
|
12
|
+
last = `HTTP ${response.status}`;
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
last = error instanceof Error ? (error.cause instanceof Error ? error.cause.message : error.message) : String(error);
|
|
16
|
+
}
|
|
17
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
18
|
+
}
|
|
19
|
+
throw new Error(`proofy: the app at ${url} did not answer in time (last: ${last})`);
|
|
20
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export { expect } from "@playwright/test";
|
|
2
|
+
/** One line of e2e/out/<run>/flows/<flow>/claims.jsonl, read by the host to build the report. */
|
|
3
|
+
export type Entry = {
|
|
4
|
+
type: "claim";
|
|
5
|
+
test: string;
|
|
6
|
+
spec: string;
|
|
7
|
+
did: string;
|
|
8
|
+
expected: string;
|
|
9
|
+
actual: string;
|
|
10
|
+
ok: boolean;
|
|
11
|
+
evidence?: unknown;
|
|
12
|
+
fakes: string[];
|
|
13
|
+
} | {
|
|
14
|
+
type: "not-covered";
|
|
15
|
+
test: string;
|
|
16
|
+
spec: string;
|
|
17
|
+
why: string;
|
|
18
|
+
} | {
|
|
19
|
+
type: "shot";
|
|
20
|
+
test: string;
|
|
21
|
+
file: string;
|
|
22
|
+
label: string;
|
|
23
|
+
};
|
|
24
|
+
export interface Claim {
|
|
25
|
+
/** The requirement number from the spec, such as "R2". */
|
|
26
|
+
spec: string;
|
|
27
|
+
/** What the test did to check it. */
|
|
28
|
+
did: string;
|
|
29
|
+
/** What should be true, in words. */
|
|
30
|
+
expected: string;
|
|
31
|
+
/** What was actually found, as read from the app, a fake or Mailpit. */
|
|
32
|
+
actual: unknown;
|
|
33
|
+
/** Whether the requirement held. Decide it from real values, never from text on the screen alone. */
|
|
34
|
+
ok: boolean;
|
|
35
|
+
/** Raw data behind the claim: a stored record, a request body. Shown in the report. */
|
|
36
|
+
evidence?: unknown;
|
|
37
|
+
/** Fakes this claim's evidence depends on. Hosts read with standin.requests() are added automatically. */
|
|
38
|
+
fakes?: string[];
|
|
39
|
+
}
|
|
40
|
+
export interface Mail {
|
|
41
|
+
id: string;
|
|
42
|
+
subject: string;
|
|
43
|
+
from: string;
|
|
44
|
+
to: string[];
|
|
45
|
+
cc: string[];
|
|
46
|
+
bcc: string[];
|
|
47
|
+
text: string;
|
|
48
|
+
html: string;
|
|
49
|
+
}
|
|
50
|
+
export interface RecordedRequest {
|
|
51
|
+
host: string;
|
|
52
|
+
method: string;
|
|
53
|
+
path: string;
|
|
54
|
+
query: Record<string, string>;
|
|
55
|
+
headers: Record<string, string>;
|
|
56
|
+
body: string;
|
|
57
|
+
handledAs: string;
|
|
58
|
+
status: number;
|
|
59
|
+
}
|
|
60
|
+
export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & {
|
|
61
|
+
proofy: void;
|
|
62
|
+
}, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
|
|
63
|
+
/** Records one requirement's result. A failed claim fails the test at its end, after every claim is recorded. */
|
|
64
|
+
export declare function claim(input: Claim): void;
|
|
65
|
+
/** Marks a requirement the sandbox cannot show, with the reason. It appears in the report, never as a pass. */
|
|
66
|
+
export declare function notCovered(spec: string, why: string): void;
|
|
67
|
+
/** A numbered full-page screenshot for the report: shot('basket with one coffee'). */
|
|
68
|
+
export declare function shot(label: string): Promise<void>;
|
|
69
|
+
/** A value unique to this test, the same on every call: use it in names, emails and phone numbers. */
|
|
70
|
+
export declare function tag(): string;
|
|
71
|
+
/** Runs a command named in e2e/proofy.json exec, in its service, with extra arguments. Returns stdout. */
|
|
72
|
+
export declare function exec(name: string, args?: string[]): Promise<string>;
|
|
73
|
+
export interface MailOptions {
|
|
74
|
+
/** Wait until at least this many messages arrived. Default 1; 0 reads without waiting. */
|
|
75
|
+
atLeast?: number;
|
|
76
|
+
/** Give up waiting after this long. Default 15 seconds. */
|
|
77
|
+
timeoutMs?: number;
|
|
78
|
+
/** After that, keep reading until nothing new arrived for this long, to catch a late extra message. Default 0. */
|
|
79
|
+
settleMs?: number;
|
|
80
|
+
}
|
|
81
|
+
export declare const mail: {
|
|
82
|
+
/**
|
|
83
|
+
* Mail delivered to one address, oldest first. Waits until at least `atLeast` messages arrived or the time ran out.
|
|
84
|
+
* Use atLeast: 0 to read without waiting, for example to check that nothing was sent.
|
|
85
|
+
*/
|
|
86
|
+
for(address: string, options?: MailOptions): Promise<Mail[]>;
|
|
87
|
+
/**
|
|
88
|
+
* Mail matching a Mailpit search, oldest first, such as an order number or `subject:"Your order"`.
|
|
89
|
+
* Every test shares the mailbox, so search for something only this test's data contains.
|
|
90
|
+
*/
|
|
91
|
+
search(query: string, options?: MailOptions): Promise<Mail[]>;
|
|
92
|
+
};
|
|
93
|
+
export declare const standin: {
|
|
94
|
+
/**
|
|
95
|
+
* Requests the app sent to an outside host, oldest first, waiting until at least `atLeast` arrived.
|
|
96
|
+
* Reading a host marks the test's later claims as depending on its fake.
|
|
97
|
+
*/
|
|
98
|
+
requests(host: string, options?: {
|
|
99
|
+
atLeast?: number;
|
|
100
|
+
timeoutMs?: number;
|
|
101
|
+
}): Promise<RecordedRequest[]>;
|
|
102
|
+
};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { test as base } from "@playwright/test";
|
|
4
|
+
import { MAILPIT_API, MOUNT, STANDIN_ADMIN } from "../sandbox/contract.js";
|
|
5
|
+
export { expect } from "@playwright/test";
|
|
6
|
+
let current;
|
|
7
|
+
export const test = base.extend({
|
|
8
|
+
proofy: [
|
|
9
|
+
async ({ page }, use, testInfo) => {
|
|
10
|
+
current = {
|
|
11
|
+
flow: basename(testInfo.file).replace(/\.spec\.[cm]?[jt]s$/, ""),
|
|
12
|
+
title: testInfo.title,
|
|
13
|
+
page,
|
|
14
|
+
tag: Math.random().toString(36).slice(2, 8),
|
|
15
|
+
shots: 0,
|
|
16
|
+
failed: [],
|
|
17
|
+
fakes: new Set(),
|
|
18
|
+
};
|
|
19
|
+
const mine = current;
|
|
20
|
+
await use();
|
|
21
|
+
current = undefined;
|
|
22
|
+
if (mine.failed.length > 0) {
|
|
23
|
+
throw new Error(`Requirements not met: ${mine.failed.join(", ")}. See the report for expected and actual values`);
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
{ auto: true },
|
|
27
|
+
],
|
|
28
|
+
});
|
|
29
|
+
/** Records one requirement's result. A failed claim fails the test at its end, after every claim is recorded. */
|
|
30
|
+
export function claim(input) {
|
|
31
|
+
const state = active("claim");
|
|
32
|
+
const fakes = [...new Set([...(input.fakes ?? []), ...state.fakes])].sort();
|
|
33
|
+
write(state, {
|
|
34
|
+
type: "claim",
|
|
35
|
+
test: state.title,
|
|
36
|
+
spec: input.spec,
|
|
37
|
+
did: input.did,
|
|
38
|
+
expected: input.expected,
|
|
39
|
+
actual: typeof input.actual === "string" ? input.actual : JSON.stringify(input.actual),
|
|
40
|
+
ok: input.ok,
|
|
41
|
+
...(input.evidence === undefined ? {} : { evidence: input.evidence }),
|
|
42
|
+
fakes,
|
|
43
|
+
});
|
|
44
|
+
if (!input.ok)
|
|
45
|
+
state.failed.push(input.spec);
|
|
46
|
+
}
|
|
47
|
+
/** Marks a requirement the sandbox cannot show, with the reason. It appears in the report, never as a pass. */
|
|
48
|
+
export function notCovered(spec, why) {
|
|
49
|
+
const state = active("notCovered");
|
|
50
|
+
write(state, { type: "not-covered", test: state.title, spec, why });
|
|
51
|
+
}
|
|
52
|
+
/** A numbered full-page screenshot for the report: shot('basket with one coffee'). */
|
|
53
|
+
export async function shot(label) {
|
|
54
|
+
const state = active("shot");
|
|
55
|
+
state.shots++;
|
|
56
|
+
const file = `${slug(state.title)}-${String(state.shots).padStart(2, "0")}-${slug(label)}.png`;
|
|
57
|
+
await state.page.screenshot({ path: join(flowDir(state), file), fullPage: true });
|
|
58
|
+
write(state, { type: "shot", test: state.title, file, label });
|
|
59
|
+
}
|
|
60
|
+
/** A value unique to this test, the same on every call: use it in names, emails and phone numbers. */
|
|
61
|
+
export function tag() {
|
|
62
|
+
return active("tag").tag;
|
|
63
|
+
}
|
|
64
|
+
/** Runs a command named in e2e/proofy.json exec, in its service, with extra arguments. Returns stdout. */
|
|
65
|
+
export async function exec(name, args = []) {
|
|
66
|
+
active("exec");
|
|
67
|
+
const id = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
68
|
+
mkdirSync(MOUNT.exec, { recursive: true });
|
|
69
|
+
writeFileSync(join(MOUNT.exec, `${id}.req.json`), JSON.stringify({ name, args }));
|
|
70
|
+
const response = join(MOUNT.exec, `${id}.res.json`);
|
|
71
|
+
const deadline = Date.now() + 120_000;
|
|
72
|
+
while (!existsSync(response)) {
|
|
73
|
+
if (Date.now() > deadline)
|
|
74
|
+
throw new Error(`exec("${name}") got no answer within 2 minutes`);
|
|
75
|
+
await sleep(100);
|
|
76
|
+
}
|
|
77
|
+
const result = JSON.parse(readFileSync(response, "utf8"));
|
|
78
|
+
rmSync(response, { force: true });
|
|
79
|
+
if (result.error)
|
|
80
|
+
throw new Error(result.error);
|
|
81
|
+
if (result.code !== 0)
|
|
82
|
+
throw new Error(`exec("${name}") exited with ${result.code}: ${result.stderr.trim() || result.stdout.trim()}`);
|
|
83
|
+
return result.stdout;
|
|
84
|
+
}
|
|
85
|
+
export const mail = {
|
|
86
|
+
/**
|
|
87
|
+
* Mail delivered to one address, oldest first. Waits until at least `atLeast` messages arrived or the time ran out.
|
|
88
|
+
* Use atLeast: 0 to read without waiting, for example to check that nothing was sent.
|
|
89
|
+
*/
|
|
90
|
+
for(address, options = {}) {
|
|
91
|
+
active("mail.for");
|
|
92
|
+
return searchMail(`to:"${address}"`, options);
|
|
93
|
+
},
|
|
94
|
+
/**
|
|
95
|
+
* Mail matching a Mailpit search, oldest first, such as an order number or `subject:"Your order"`.
|
|
96
|
+
* Every test shares the mailbox, so search for something only this test's data contains.
|
|
97
|
+
*/
|
|
98
|
+
search(query, options = {}) {
|
|
99
|
+
active("mail.search");
|
|
100
|
+
return searchMail(query, options);
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
async function searchMail(query, options) {
|
|
104
|
+
const atLeast = options.atLeast ?? 1;
|
|
105
|
+
const deadline = Date.now() + (options.timeoutMs ?? 15_000);
|
|
106
|
+
const ids = async () => {
|
|
107
|
+
const search = (await getJson(`${MAILPIT_API}/api/v1/search?query=${encodeURIComponent(query)}`));
|
|
108
|
+
return (search.messages ?? []).map((message) => message.ID);
|
|
109
|
+
};
|
|
110
|
+
let found = await ids();
|
|
111
|
+
while (found.length < atLeast && Date.now() < deadline) {
|
|
112
|
+
await sleep(500);
|
|
113
|
+
found = await ids();
|
|
114
|
+
}
|
|
115
|
+
if (options.settleMs) {
|
|
116
|
+
let stableSince = Date.now();
|
|
117
|
+
while (Date.now() - stableSince < options.settleMs) {
|
|
118
|
+
await sleep(Math.min(500, options.settleMs));
|
|
119
|
+
const again = await ids();
|
|
120
|
+
if (again.length !== found.length) {
|
|
121
|
+
found = again;
|
|
122
|
+
stableSince = Date.now();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const messages = await Promise.all(found.map((id) => getJson(`${MAILPIT_API}/api/v1/message/${id}`)));
|
|
127
|
+
return messages.map(toMail).reverse();
|
|
128
|
+
}
|
|
129
|
+
export const standin = {
|
|
130
|
+
/**
|
|
131
|
+
* Requests the app sent to an outside host, oldest first, waiting until at least `atLeast` arrived.
|
|
132
|
+
* Reading a host marks the test's later claims as depending on its fake.
|
|
133
|
+
*/
|
|
134
|
+
async requests(host, options = {}) {
|
|
135
|
+
const state = active("standin.requests");
|
|
136
|
+
state.fakes.add(host);
|
|
137
|
+
const atLeast = options.atLeast ?? 0;
|
|
138
|
+
const deadline = Date.now() + (options.timeoutMs ?? 15_000);
|
|
139
|
+
for (;;) {
|
|
140
|
+
const requests = (await getJson(`${STANDIN_ADMIN}/requests?host=${encodeURIComponent(host)}`));
|
|
141
|
+
if (requests.length >= atLeast || Date.now() > deadline)
|
|
142
|
+
return requests;
|
|
143
|
+
await sleep(250);
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
function active(name) {
|
|
148
|
+
if (!current)
|
|
149
|
+
throw new Error(`${name}() must be called inside a test that imports test from "@davidrydberg/proofy/test"`);
|
|
150
|
+
return current;
|
|
151
|
+
}
|
|
152
|
+
function flowDir(state) {
|
|
153
|
+
const dir = join(MOUNT.out, "flows", state.flow);
|
|
154
|
+
mkdirSync(dir, { recursive: true });
|
|
155
|
+
return dir;
|
|
156
|
+
}
|
|
157
|
+
function write(state, entry) {
|
|
158
|
+
appendFileSync(join(flowDir(state), "claims.jsonl"), `${JSON.stringify(entry)}\n`);
|
|
159
|
+
}
|
|
160
|
+
async function getJson(url) {
|
|
161
|
+
const response = await fetch(url);
|
|
162
|
+
if (!response.ok)
|
|
163
|
+
throw new Error(`${url} answered ${response.status}`);
|
|
164
|
+
return response.json();
|
|
165
|
+
}
|
|
166
|
+
function toMail(raw) {
|
|
167
|
+
const message = raw;
|
|
168
|
+
return {
|
|
169
|
+
id: message.ID,
|
|
170
|
+
subject: message.Subject,
|
|
171
|
+
from: message.From?.Address ?? "",
|
|
172
|
+
to: (message.To ?? []).map((to) => to.Address),
|
|
173
|
+
cc: (message.Cc ?? []).map((cc) => cc.Address),
|
|
174
|
+
bcc: (message.Bcc ?? []).map((bcc) => bcc.Address),
|
|
175
|
+
text: message.Text,
|
|
176
|
+
html: message.HTML,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function slug(text) {
|
|
180
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 50) || "step";
|
|
181
|
+
}
|
|
182
|
+
function sleep(ms) {
|
|
183
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
184
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,70 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@davidrydberg/proofy",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Sealed end-to-end tests that your coding agent writes and you approve: requirements in plain words, evidence and screenshots per requirement.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/davidrydberg/proofy.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/davidrydberg/proofy#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/davidrydberg/proofy/issues"
|
|
13
|
+
},
|
|
14
|
+
"author": "David Rydberg <david@davidrydberg.dev>",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"bin": {
|
|
17
|
+
"proofy": "dist/bin/proofy.js"
|
|
18
|
+
},
|
|
19
|
+
"exports": {
|
|
20
|
+
"./test": {
|
|
21
|
+
"types": "./dist/test/index.d.ts",
|
|
22
|
+
"default": "./dist/test/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./package.json": "./package.json"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"docker",
|
|
29
|
+
"schema",
|
|
30
|
+
"templates"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=24"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"cli",
|
|
40
|
+
"agents",
|
|
41
|
+
"e2e",
|
|
42
|
+
"end-to-end",
|
|
43
|
+
"playwright",
|
|
44
|
+
"docker",
|
|
45
|
+
"testing"
|
|
46
|
+
],
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "rm -rf dist schema && tsc -p tsconfig.build.json && chmod +x dist/bin/proofy.js && node scripts/schema.mjs",
|
|
49
|
+
"typecheck": "tsc --noEmit",
|
|
50
|
+
"lint": "eslint .",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"prepublishOnly": "npm run typecheck && npm run lint && npm test && npm run build",
|
|
53
|
+
"test:docker": "npm run build && vitest run --config vitest.docker.config.ts"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@playwright/test": "1.64.1",
|
|
57
|
+
"@toon-format/toon": "^4.4.0",
|
|
58
|
+
"yaml": "^2.9.1",
|
|
59
|
+
"zod": "^4.6.7"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@eslint/js": "^10.0.1",
|
|
63
|
+
"@types/node": "^26.6.5",
|
|
64
|
+
"eslint": "^10.13.0",
|
|
65
|
+
"globals": "^17.13.0",
|
|
66
|
+
"typescript": "^6.0.3",
|
|
67
|
+
"typescript-eslint": "^8.71.2",
|
|
68
|
+
"vitest": "^5.0.5"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"properties": {
|
|
5
|
+
"$schema": {
|
|
6
|
+
"type": "string"
|
|
7
|
+
},
|
|
8
|
+
"compose": {
|
|
9
|
+
"default": [],
|
|
10
|
+
"description": "The app's compose files, relative to the repository root",
|
|
11
|
+
"type": "array",
|
|
12
|
+
"items": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"minLength": 1
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"app": {
|
|
18
|
+
"description": "Where the browser starts",
|
|
19
|
+
"type": "object",
|
|
20
|
+
"properties": {
|
|
21
|
+
"service": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"minLength": 1,
|
|
24
|
+
"description": "Compose service the browser opens"
|
|
25
|
+
},
|
|
26
|
+
"port": {
|
|
27
|
+
"type": "integer",
|
|
28
|
+
"minimum": 1,
|
|
29
|
+
"maximum": 65535,
|
|
30
|
+
"description": "Port inside the container, not a published port"
|
|
31
|
+
},
|
|
32
|
+
"path": {
|
|
33
|
+
"default": "/",
|
|
34
|
+
"type": "string",
|
|
35
|
+
"format": "starts_with",
|
|
36
|
+
"pattern": "^\\/.*"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"required": [
|
|
40
|
+
"service",
|
|
41
|
+
"port"
|
|
42
|
+
],
|
|
43
|
+
"additionalProperties": false
|
|
44
|
+
},
|
|
45
|
+
"seed": {
|
|
46
|
+
"description": "Runs once on the empty database before the tests",
|
|
47
|
+
"type": "object",
|
|
48
|
+
"properties": {
|
|
49
|
+
"service": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"minLength": 1,
|
|
52
|
+
"description": "Compose service to run it in"
|
|
53
|
+
},
|
|
54
|
+
"command": {
|
|
55
|
+
"minItems": 1,
|
|
56
|
+
"type": "array",
|
|
57
|
+
"items": {
|
|
58
|
+
"type": "string",
|
|
59
|
+
"minLength": 1
|
|
60
|
+
},
|
|
61
|
+
"description": "Command and arguments, run without a shell"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"required": [
|
|
65
|
+
"service",
|
|
66
|
+
"command"
|
|
67
|
+
],
|
|
68
|
+
"additionalProperties": false
|
|
69
|
+
},
|
|
70
|
+
"exec": {
|
|
71
|
+
"default": {},
|
|
72
|
+
"description": "Commands tests may run with exec(name), such as a scheduled job",
|
|
73
|
+
"type": "object",
|
|
74
|
+
"propertyNames": {
|
|
75
|
+
"type": "string",
|
|
76
|
+
"minLength": 1
|
|
77
|
+
},
|
|
78
|
+
"additionalProperties": {
|
|
79
|
+
"type": "object",
|
|
80
|
+
"properties": {
|
|
81
|
+
"service": {
|
|
82
|
+
"type": "string",
|
|
83
|
+
"minLength": 1,
|
|
84
|
+
"description": "Compose service to run it in"
|
|
85
|
+
},
|
|
86
|
+
"command": {
|
|
87
|
+
"minItems": 1,
|
|
88
|
+
"type": "array",
|
|
89
|
+
"items": {
|
|
90
|
+
"type": "string",
|
|
91
|
+
"minLength": 1
|
|
92
|
+
},
|
|
93
|
+
"description": "Command and arguments, run without a shell"
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"required": [
|
|
97
|
+
"service",
|
|
98
|
+
"command"
|
|
99
|
+
],
|
|
100
|
+
"additionalProperties": false
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"hosts": {
|
|
104
|
+
"default": {},
|
|
105
|
+
"description": "Outside hosts: fake answers from e2e/standins/<host>.mjs, record accepts everything. Unlisted hosts are refused",
|
|
106
|
+
"type": "object",
|
|
107
|
+
"propertyNames": {
|
|
108
|
+
"type": "string",
|
|
109
|
+
"pattern": "^(?=.{1,253}$)([a-z0-9]([a-z0-9-]*[a-z0-9])?\\.)+[a-z]{2,63}$"
|
|
110
|
+
},
|
|
111
|
+
"additionalProperties": {
|
|
112
|
+
"type": "string",
|
|
113
|
+
"enum": [
|
|
114
|
+
"fake",
|
|
115
|
+
"record"
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
"assets": {
|
|
120
|
+
"default": [],
|
|
121
|
+
"description": "Static third-party files downloaded at install and served in the sealed run",
|
|
122
|
+
"type": "array",
|
|
123
|
+
"items": {
|
|
124
|
+
"type": "string",
|
|
125
|
+
"format": "uri"
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
"env": {
|
|
129
|
+
"default": {},
|
|
130
|
+
"description": "Settings per compose service. The repository's .env is never read",
|
|
131
|
+
"type": "object",
|
|
132
|
+
"propertyNames": {
|
|
133
|
+
"type": "string",
|
|
134
|
+
"minLength": 1
|
|
135
|
+
},
|
|
136
|
+
"additionalProperties": {
|
|
137
|
+
"type": "object",
|
|
138
|
+
"propertyNames": {
|
|
139
|
+
"type": "string",
|
|
140
|
+
"minLength": 1
|
|
141
|
+
},
|
|
142
|
+
"additionalProperties": {
|
|
143
|
+
"type": "string"
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
"limits": {
|
|
148
|
+
"default": {},
|
|
149
|
+
"type": "object",
|
|
150
|
+
"properties": {
|
|
151
|
+
"startMinutes": {
|
|
152
|
+
"default": 10,
|
|
153
|
+
"type": "number",
|
|
154
|
+
"exclusiveMinimum": 0
|
|
155
|
+
},
|
|
156
|
+
"testMinutes": {
|
|
157
|
+
"default": 5,
|
|
158
|
+
"type": "number",
|
|
159
|
+
"exclusiveMinimum": 0
|
|
160
|
+
},
|
|
161
|
+
"maxRunMinutes": {
|
|
162
|
+
"type": "number",
|
|
163
|
+
"exclusiveMinimum": 0
|
|
164
|
+
},
|
|
165
|
+
"parallel": {
|
|
166
|
+
"default": 4,
|
|
167
|
+
"type": "integer",
|
|
168
|
+
"minimum": 1,
|
|
169
|
+
"maximum": 9007199254740991
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
"additionalProperties": false
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
"additionalProperties": false,
|
|
176
|
+
"description": "proofy configuration, e2e/proofy.json",
|
|
177
|
+
"$id": "https://unpkg.com/@davidrydberg/proofy/schema/proofy.schema.json",
|
|
178
|
+
"title": "proofy config"
|
|
179
|
+
}
|