@volter/twin-runhuman 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 +202 -0
- package/README.md +81 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +25 -0
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.js +44 -0
- package/dist/src/runhuman-budget.d.ts +23 -0
- package/dist/src/runhuman-budget.js +45 -0
- package/dist/src/runhuman-capabilities.d.ts +7 -0
- package/dist/src/runhuman-capabilities.js +451 -0
- package/dist/src/runhuman-conformance.d.ts +10 -0
- package/dist/src/runhuman-conformance.js +11 -0
- package/dist/src/runhuman-connector.d.ts +32 -0
- package/dist/src/runhuman-connector.js +58 -0
- package/dist/src/runhuman-server.d.ts +13 -0
- package/dist/src/runhuman-server.js +25 -0
- package/dist/src/runhuman-twin.d.ts +16 -0
- package/dist/src/runhuman-twin.js +1162 -0
- package/dist/src/runhuman-validate.d.ts +40 -0
- package/dist/src/runhuman-validate.js +161 -0
- package/package.json +51 -0
- package/src/cli.ts +24 -0
- package/src/index.ts +61 -0
- package/src/runhuman-budget.ts +66 -0
- package/src/runhuman-capabilities.ts +470 -0
- package/src/runhuman-conformance.ts +14 -0
- package/src/runhuman-connector.ts +67 -0
- package/src/runhuman-server.ts +32 -0
- package/src/runhuman-twin.ts +1078 -0
- package/src/runhuman-validate.ts +185 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// Runhuman 1 request-body validation, transcribed from RH1's own boundary schemas
|
|
2
|
+
// (volter-ai/runhuman packages/shared/src/contracts/job.schema.ts + tester-job.schema.ts, zod ^3)
|
|
3
|
+
// and its Fastify validator compiler (packages/api/src/utils/zod-validator-compiler.ts):
|
|
4
|
+
//
|
|
5
|
+
// - an absent body validates as `{}`;
|
|
6
|
+
// - every issue is reported, joined by "; ", each as "<path>: <message>" (path dot-joined);
|
|
7
|
+
// - the whole string is prefixed "Invalid request body: ";
|
|
8
|
+
// - Fastify stamps the error statusCode 400 / code FST_ERR_VALIDATION, and RH1's global error
|
|
9
|
+
// handler serialises it as `{ error, statusCode, code }`.
|
|
10
|
+
//
|
|
11
|
+
// Messages are zod v3's own: "Required", "Expected string, received number",
|
|
12
|
+
// "Invalid enum value. Expected 'a' | 'b', received 'c'", and "Invalid input" for a
|
|
13
|
+
// `z.custom` (RH1's `objectGuard`) that rejects.
|
|
14
|
+
import type { RunhumanTwinResponse } from './runhuman-twin.ts';
|
|
15
|
+
|
|
16
|
+
export type FieldKind =
|
|
17
|
+
| { kind: 'string' }
|
|
18
|
+
| { kind: 'number' }
|
|
19
|
+
| { kind: 'boolean' }
|
|
20
|
+
| { kind: 'enum'; values: readonly string[] }
|
|
21
|
+
| { kind: 'object' } // RH1 objectGuard: plain object, not an array
|
|
22
|
+
| { kind: 'array'; of: FieldKind }
|
|
23
|
+
| { kind: 'unknown' };
|
|
24
|
+
|
|
25
|
+
export type FieldSpec = FieldKind & { required?: boolean; refine?: (value: unknown) => string | undefined };
|
|
26
|
+
|
|
27
|
+
function zodType(value: unknown): string {
|
|
28
|
+
if (value === null) return 'null';
|
|
29
|
+
if (Array.isArray(value)) return 'array';
|
|
30
|
+
if (typeof value === 'number' && Number.isNaN(value)) return 'nan';
|
|
31
|
+
return typeof value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function checkKind(value: unknown, kind: FieldKind, path: string, issues: string[]): void {
|
|
35
|
+
const at = (message: string) => issues.push(path ? `${path}: ${message}` : message);
|
|
36
|
+
switch (kind.kind) {
|
|
37
|
+
case 'unknown':
|
|
38
|
+
return;
|
|
39
|
+
case 'string':
|
|
40
|
+
case 'number':
|
|
41
|
+
case 'boolean':
|
|
42
|
+
if (typeof value !== kind.kind || (kind.kind === 'number' && Number.isNaN(value))) at(`Expected ${kind.kind}, received ${zodType(value)}`);
|
|
43
|
+
return;
|
|
44
|
+
case 'enum':
|
|
45
|
+
if (typeof value !== 'string') {
|
|
46
|
+
at(`Expected ${kind.values.map((v) => `'${v}'`).join(' | ')}, received ${zodType(value)}`);
|
|
47
|
+
} else if (!kind.values.includes(value)) {
|
|
48
|
+
at(`Invalid enum value. Expected ${kind.values.map((v) => `'${v}'`).join(' | ')}, received '${value}'`);
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
case 'object':
|
|
52
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) at('Invalid input');
|
|
53
|
+
return;
|
|
54
|
+
case 'array':
|
|
55
|
+
if (!Array.isArray(value)) {
|
|
56
|
+
at(`Expected array, received ${zodType(value)}`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
value.forEach((el, i) => checkKind(el, kind.of, path ? `${path}.${i}` : String(i), issues));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Validate a parsed JSON body against a z.object-shaped field map (unknown keys are stripped, as
|
|
65
|
+
* zod's default object does). Returns the 400 RH1 answers, or undefined when the body passes. */
|
|
66
|
+
export function validateBody(body: unknown, fields: Record<string, FieldSpec>): RunhumanTwinResponse | undefined {
|
|
67
|
+
const input = body === undefined || body === null ? {} : body;
|
|
68
|
+
const issues: string[] = [];
|
|
69
|
+
if (typeof input !== 'object' || Array.isArray(input)) {
|
|
70
|
+
issues.push(`Expected object, received ${zodType(input)}`);
|
|
71
|
+
} else {
|
|
72
|
+
const record = input as Record<string, unknown>;
|
|
73
|
+
for (const [name, spec] of Object.entries(fields)) {
|
|
74
|
+
const value = record[name];
|
|
75
|
+
if (value === undefined) {
|
|
76
|
+
if (spec.required) issues.push(`${name}: Required`);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const before = issues.length;
|
|
80
|
+
checkKind(value, spec, name, issues);
|
|
81
|
+
if (issues.length === before && spec.refine) {
|
|
82
|
+
const message = spec.refine(value);
|
|
83
|
+
if (message) issues.push(`${name}: ${message}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (issues.length === 0) return undefined;
|
|
88
|
+
const error = `Invalid request body: ${issues.join('; ')}`;
|
|
89
|
+
return { status: 400, body: { error, statusCode: 400, code: 'FST_ERR_VALIDATION' } };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** A `z.custom` objectGuard over the whole body (RH1's PATCH /tester/jobs/:jobId schema). */
|
|
93
|
+
export function validateObjectBody(body: unknown): RunhumanTwinResponse | undefined {
|
|
94
|
+
const input = body === undefined || body === null ? {} : body;
|
|
95
|
+
if (typeof input === 'object' && !Array.isArray(input)) return undefined;
|
|
96
|
+
return { status: 400, body: { error: 'Invalid request body: Invalid input', statusCode: 400, code: 'FST_ERR_VALIDATION' } };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const S = { kind: 'string' } as const;
|
|
100
|
+
const N = { kind: 'number' } as const;
|
|
101
|
+
const B = { kind: 'boolean' } as const;
|
|
102
|
+
const O = { kind: 'object' } as const;
|
|
103
|
+
const PREF = { kind: 'enum', values: ['inherit', 'always', 'never'] } as const;
|
|
104
|
+
|
|
105
|
+
// packages/shared/src/job/device-class.types.ts, tester/tester.types.ts (TESTER_DEVICES,
|
|
106
|
+
// TESTER_LANGUAGES, CLAIM_SOURCES).
|
|
107
|
+
export const DEVICE_CLASSES = ['desktop', 'mobile', 'both'] as const;
|
|
108
|
+
export const TESTER_DEVICES = ['ios', 'android', 'pc', 'mac'] as const;
|
|
109
|
+
export const TESTER_LANGUAGES = ['english', 'spanish'] as const;
|
|
110
|
+
export const CLAIM_SOURCES = ['tester-portal', 'mobile-app', 'slack', 'discord-activity'] as const;
|
|
111
|
+
|
|
112
|
+
/** createTestJobRequestSchema (POST /api/jobs). */
|
|
113
|
+
export const CREATE_JOB_FIELDS: Record<string, FieldSpec> = {
|
|
114
|
+
projectId: S,
|
|
115
|
+
organizationId: S,
|
|
116
|
+
url: S,
|
|
117
|
+
description: S,
|
|
118
|
+
template: S,
|
|
119
|
+
templateContent: S,
|
|
120
|
+
targetDurationMinutes: N,
|
|
121
|
+
maxExtensionMinutes: N,
|
|
122
|
+
outputSchema: O,
|
|
123
|
+
resultsTemplate: S,
|
|
124
|
+
deviceClass: { kind: 'enum', values: DEVICE_CLASSES },
|
|
125
|
+
screenSize: { kind: 'unknown' },
|
|
126
|
+
attachments: { kind: 'array', of: O },
|
|
127
|
+
metadata: {
|
|
128
|
+
kind: 'object',
|
|
129
|
+
refine: (value) => {
|
|
130
|
+
const captureMode = (value as Record<string, unknown>).captureMode;
|
|
131
|
+
return captureMode === undefined || captureMode === 'supplier' || captureMode === 'external'
|
|
132
|
+
? undefined
|
|
133
|
+
: 'metadata.captureMode must be supplier or external';
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
additionalValidationInstructions: S,
|
|
137
|
+
githubRepos: { kind: 'array', of: S },
|
|
138
|
+
githubRepo: S,
|
|
139
|
+
autoCreateGithubIssuesRepo: S,
|
|
140
|
+
githubToken: S,
|
|
141
|
+
commitSha: S,
|
|
142
|
+
enableCodeContext: B,
|
|
143
|
+
prNumbers: { kind: 'array', of: N },
|
|
144
|
+
issueNumbers: { kind: 'array', of: N },
|
|
145
|
+
checkTestability: B,
|
|
146
|
+
requiredDevices: { kind: 'array', of: { kind: 'enum', values: TESTER_DEVICES } },
|
|
147
|
+
requiredLanguages: { kind: 'array', of: { kind: 'enum', values: TESTER_LANGUAGES } },
|
|
148
|
+
requireSocialVideos: B,
|
|
149
|
+
requireSideload: B,
|
|
150
|
+
requiresRunhumanApkInstall: B,
|
|
151
|
+
autoCreateGithubIssues: B,
|
|
152
|
+
autoCreateGithubFeedback: B,
|
|
153
|
+
autoCreateJiraFeedback: B,
|
|
154
|
+
autoCreateJiraIssues: B,
|
|
155
|
+
autoCreateLinearIssues: B,
|
|
156
|
+
enhancedVideo: B,
|
|
157
|
+
enhanceInstructions: B,
|
|
158
|
+
autoCreateOnlyTesterSurfaced: B,
|
|
159
|
+
reopenIssuesOnDuplicate: B,
|
|
160
|
+
commentOnDuplicate: B,
|
|
161
|
+
slackNotification: O,
|
|
162
|
+
emailOnCompletion: PREF,
|
|
163
|
+
inAppOnCompletion: PREF,
|
|
164
|
+
emailOnFailure: PREF,
|
|
165
|
+
inAppOnFailure: PREF,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/** claimJobRequestSchema (POST /api/jobs/:jobId/claim): `claimSource` is REQUIRED by the schema
|
|
169
|
+
* even though the handler carries a default. */
|
|
170
|
+
export const CLAIM_JOB_FIELDS: Record<string, FieldSpec> = {
|
|
171
|
+
claimSource: { kind: 'enum', values: CLAIM_SOURCES, required: true },
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/** endJobRequestSchema (POST /api/tester/jobs/:jobId/end). */
|
|
175
|
+
export const END_JOB_FIELDS: Record<string, FieldSpec> = {
|
|
176
|
+
action: { kind: 'enum', values: ['clear_and_release', 'clear_and_cancel', 'save_and_cancel', 'platform_issue'], required: true },
|
|
177
|
+
note: { kind: 'string', required: true },
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/** processTestResultsBodySchema (POST /api/tester/jobs/:jobId/process-results). */
|
|
181
|
+
export const PROCESS_RESULTS_FIELDS: Record<string, FieldSpec> = {
|
|
182
|
+
job: O,
|
|
183
|
+
prefillTemplate: S,
|
|
184
|
+
clientInfo: O,
|
|
185
|
+
};
|