owlrunkit 0.13.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/CHANGELOG.md +9 -0
- package/LICENSE +674 -0
- package/NOTICE.md +137 -0
- package/README.md +44 -0
- package/SOURCE.md +10 -0
- package/docs/architecture/OWLCODA_RUN_KIT_CONTRACT_V0_1.md +160 -0
- package/docs/architecture/OWLCODA_RUN_KIT_CONTRACT_V0_2.md +459 -0
- package/integrations/codex/skills/owlcoda-runkit/SKILL.md +408 -0
- package/integrations/codex/skills/owlcoda-runkit/agents/openai.yaml +4 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/agents-snippet.md +7 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +10 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/coverage-adopt-request.json +26 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/execution-delivery-packet.json +13 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/finalize-request.json +18 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/goal-contract.json +11 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/profiles.json +9 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/ready-for-commit-request.json +11 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/resource-preflight-request.json +56 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/resume-request.json +10 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/snapshot-request.json +23 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/verification-context.json +15 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/verification-receipt.json +11 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/verify-plan-request.json +31 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/visual-smoke-request.json +32 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/worker-lease.json +7 -0
- package/package.json +24 -0
- package/packages/attest/LICENSE +674 -0
- package/packages/attest/README.md +63 -0
- package/packages/attest/cli/owlcoda-attest.mjs +155 -0
- package/packages/attest/consumer-surface-v1.json +255 -0
- package/packages/attest/distribution-manifest-v1.json +104 -0
- package/packages/attest/fixtures/bad/accepted-authority-leak.json +7 -0
- package/packages/attest/fixtures/bad/accepted-failed-receipt.json +7 -0
- package/packages/attest/fixtures/bad/active-context-binding-mismatch.json +7 -0
- package/packages/attest/fixtures/bad/active-source-binding-mismatch.json +7 -0
- package/packages/attest/fixtures/bad/context-fingerprint-mismatch.json +7 -0
- package/packages/attest/fixtures/bad/duplicate-object-key.json +8 -0
- package/packages/attest/fixtures/bad/multiple-active-leaves.json +10 -0
- package/packages/attest/fixtures/bad/orphan-replacement.json +7 -0
- package/packages/attest/fixtures/bad/root-shape-extra-property.json +8 -0
- package/packages/attest/fixtures/bad/source-fingerprint-mismatch.json +7 -0
- package/packages/attest/fixtures/good/accepted-replacement-chain.json +59 -0
- package/packages/attest/fixtures/good/blocked-single-leaf.json +9 -0
- package/packages/attest/fixtures/index.json +90 -0
- package/packages/attest/sbom.spdx.json +33 -0
- package/packages/attest/schemas/attestation-ref-v1.schema.json +44 -0
- package/packages/attest/schemas/attestation-result-v1.schema.json +163 -0
- package/packages/attest/schemas/offline-attestation-bundle-v1.schema.json +30 -0
- package/packages/attest/schemas/public-verification-bundle-v1.schema.json +74 -0
- package/packages/attest/schemas/quick-verification-receipt-v1.schema.json +219 -0
- package/packages/attest/schemas/workspace-snapshot-v1.schema.json +135 -0
- package/packages/attest/src/canonical.mjs +12 -0
- package/packages/attest/src/formal.mjs +762 -0
- package/packages/attest/src/index.mjs +113 -0
- package/packages/attest/src/offline-bundle.mjs +108 -0
- package/packages/attest/src/quick-receipt-contract.mjs +156 -0
- package/packages/attest/src/quick.mjs +274 -0
- package/packages/attest/src/reference-contract.mjs +64 -0
- package/packages/attest/src/reference.mjs +131 -0
- package/packages/attest/src/workspace-snapshot.mjs +171 -0
- package/registry-release-policy-v1.json +34 -0
- package/sbom.spdx.json +39 -0
- package/scripts/runkit-contract/acceptance-evidence.mjs +103 -0
- package/scripts/runkit-contract/core-contract.mjs +392 -0
- package/scripts/runkit-contract/coverage-adoption.mjs +414 -0
- package/scripts/runkit-contract/delivery-create.mjs +252 -0
- package/scripts/runkit-contract/delivery-selection.mjs +31 -0
- package/scripts/runkit-contract/finalize.mjs +790 -0
- package/scripts/runkit-contract/inspect-presentation.mjs +183 -0
- package/scripts/runkit-contract/install-codex-skill.mjs +239 -0
- package/scripts/runkit-contract/lease-lifecycle.mjs +324 -0
- package/scripts/runkit-contract/lifecycle-orchestration.mjs +415 -0
- package/scripts/runkit-contract/offline-store.mjs +248 -0
- package/scripts/runkit-contract/profile-impact.mjs +223 -0
- package/scripts/runkit-contract/provenance-common.mjs +153 -0
- package/scripts/runkit-contract/quick-attest.mjs +4 -0
- package/scripts/runkit-contract/quick-canonical.mjs +41 -0
- package/scripts/runkit-contract/quick-metrics.mjs +86 -0
- package/scripts/runkit-contract/quick-receipt.mjs +139 -0
- package/scripts/runkit-contract/quick-verify.mjs +201 -0
- package/scripts/runkit-contract/quick-workspace-snapshot.mjs +3 -0
- package/scripts/runkit-contract/ready-for-commit.mjs +188 -0
- package/scripts/runkit-contract/receipt-lineage.mjs +332 -0
- package/scripts/runkit-contract/registry-adoption-gate.mjs +117 -0
- package/scripts/runkit-contract/repair-execution.mjs +997 -0
- package/scripts/runkit-contract/resource-preflight.mjs +829 -0
- package/scripts/runkit-contract/resume-execution.mjs +569 -0
- package/scripts/runkit-contract/runkit-cli.mjs +1214 -0
- package/scripts/runkit-contract/schemas/closeout-receipt-v2.schema.json +42 -0
- package/scripts/runkit-contract/schemas/core-artifact-v2.schema.json +23 -0
- package/scripts/runkit-contract/schemas/core-artifact.schema.json +23 -0
- package/scripts/runkit-contract/schemas/coverage-adopt-request-v1.schema.json +66 -0
- package/scripts/runkit-contract/schemas/engine-pin-v2.schema.json +14 -0
- package/scripts/runkit-contract/schemas/engine-pin.schema.json +14 -0
- package/scripts/runkit-contract/schemas/evidence-coverage-index-v1.schema.json +131 -0
- package/scripts/runkit-contract/schemas/finalize-request-v1.schema.json +131 -0
- package/scripts/runkit-contract/schemas/profiles.schema.json +47 -0
- package/scripts/runkit-contract/schemas/project-config-v2.schema.json +13 -0
- package/scripts/runkit-contract/schemas/project-config.schema.json +13 -0
- package/scripts/runkit-contract/schemas/ready-for-commit-v1.schema.json +26 -0
- package/scripts/runkit-contract/schemas/replayable-evidence-v1.schema.json +82 -0
- package/scripts/runkit-contract/schemas/resource-preflight-request-v1.schema.json +178 -0
- package/scripts/runkit-contract/schemas/resource-preflight-v1.schema.json +153 -0
- package/scripts/runkit-contract/schemas/resume-attempt-v1.schema.json +80 -0
- package/scripts/runkit-contract/schemas/resume-request-v1.schema.json +77 -0
- package/scripts/runkit-contract/schemas/snapshot-v1.schema.json +71 -0
- package/scripts/runkit-contract/schemas/verification-context-v1.schema.json +92 -0
- package/scripts/runkit-contract/schemas/verification-plan-v1.schema.json +150 -0
- package/scripts/runkit-contract/schemas/verification-receipt-v2.schema.json +118 -0
- package/scripts/runkit-contract/schemas/verify-plan-request-v1.schema.json +56 -0
- package/scripts/runkit-contract/schemas/visual-smoke-request-v1.schema.json +64 -0
- package/scripts/runkit-contract/schemas/visual-smoke-result-v1.schema.json +68 -0
- package/scripts/runkit-contract/snapshot.mjs +340 -0
- package/scripts/runkit-contract/source-fingerprint.mjs +404 -0
- package/scripts/runkit-contract/templates/coverage-adopt-request.json +26 -0
- package/scripts/runkit-contract/templates/finalize-request.json +18 -0
- package/scripts/runkit-contract/templates/goal-contract.json +11 -0
- package/scripts/runkit-contract/templates/ready-for-commit-request.json +11 -0
- package/scripts/runkit-contract/templates/resource-preflight-request.json +56 -0
- package/scripts/runkit-contract/templates/resume-request.json +10 -0
- package/scripts/runkit-contract/templates/snapshot-request.json +23 -0
- package/scripts/runkit-contract/templates/verify-plan-request.json +31 -0
- package/scripts/runkit-contract/templates/visual-smoke-request.json +32 -0
- package/scripts/runkit-contract/templates/worker-lease.json +7 -0
- package/scripts/runkit-contract/verification-context.mjs +141 -0
- package/scripts/runkit-contract/verification-plan.mjs +608 -0
- package/scripts/runkit-contract/verification-receipt-gate.mjs +566 -0
- package/scripts/runkit-contract/visual-smoke.mjs +410 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
lstatSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
realpathSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
|
|
12
|
+
import { captureTargetState } from "./snapshot.mjs";
|
|
13
|
+
import {
|
|
14
|
+
assertAllowedKeys,
|
|
15
|
+
isRecord,
|
|
16
|
+
loadActiveExecution,
|
|
17
|
+
relativeToWorkspace,
|
|
18
|
+
resolveWithinRoot,
|
|
19
|
+
safeIdentifier,
|
|
20
|
+
safeRelativePath,
|
|
21
|
+
sha256,
|
|
22
|
+
writeJsonExclusive,
|
|
23
|
+
} from "./provenance-common.mjs";
|
|
24
|
+
import {
|
|
25
|
+
validateVerificationContext,
|
|
26
|
+
verificationContextFingerprint,
|
|
27
|
+
} from "./verification-context.mjs";
|
|
28
|
+
|
|
29
|
+
const STATUS_MODE = "porcelain-v1-z-untracked-all-runkit-excluded";
|
|
30
|
+
const REQUEST_KEYS = [
|
|
31
|
+
"schemaVersion",
|
|
32
|
+
"smokeId",
|
|
33
|
+
"mode",
|
|
34
|
+
"targetRoot",
|
|
35
|
+
"cwd",
|
|
36
|
+
"executable",
|
|
37
|
+
"argv",
|
|
38
|
+
"launcherVersion",
|
|
39
|
+
"toolVersions",
|
|
40
|
+
"selectedPaths",
|
|
41
|
+
"statusMode",
|
|
42
|
+
"verificationContext",
|
|
43
|
+
"resultPath",
|
|
44
|
+
"automationManifest",
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
function canonicalJson(value) {
|
|
48
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
49
|
+
if (value && typeof value === "object") {
|
|
50
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
|
|
51
|
+
}
|
|
52
|
+
return JSON.stringify(value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function lineCount(bytes) {
|
|
56
|
+
if (bytes.length === 0) return 0;
|
|
57
|
+
const text = bytes.toString("utf8");
|
|
58
|
+
return text.endsWith("\n") ? text.slice(0, -1).split("\n").length : text.split("\n").length;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function targetStable(before, after) {
|
|
62
|
+
return before.head === after.head
|
|
63
|
+
&& before.statusSha256 === after.statusSha256
|
|
64
|
+
&& before.manifestFingerprint === after.manifestFingerprint;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function withinRoot(root, candidate) {
|
|
68
|
+
const remainder = path.relative(root, candidate);
|
|
69
|
+
return remainder === ""
|
|
70
|
+
|| (!remainder.startsWith(`..${path.sep}`) && remainder !== ".." && !path.isAbsolute(remainder));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function validateNonNegativeInteger(value, label) {
|
|
74
|
+
if (!Number.isInteger(value) || value < 0) throw new Error(`${label} must be a non-negative integer.`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function validateAutomationManifest(manifest) {
|
|
78
|
+
assertAllowedKeys(manifest, "automationManifest", [
|
|
79
|
+
"schemaVersion",
|
|
80
|
+
"surface",
|
|
81
|
+
"viewport",
|
|
82
|
+
"assertionIds",
|
|
83
|
+
"consolePolicy",
|
|
84
|
+
"externalNavigationPolicy",
|
|
85
|
+
]);
|
|
86
|
+
if (manifest.schemaVersion !== "OwlCodaRunKitVisualAutomationManifestV1") {
|
|
87
|
+
throw new Error("Unsupported automationManifest schemaVersion.");
|
|
88
|
+
}
|
|
89
|
+
safeIdentifier(manifest.surface, "automationManifest surface");
|
|
90
|
+
assertAllowedKeys(manifest.viewport, "automationManifest viewport", ["width", "height"]);
|
|
91
|
+
if (!Number.isInteger(manifest.viewport.width) || manifest.viewport.width < 1
|
|
92
|
+
|| !Number.isInteger(manifest.viewport.height) || manifest.viewport.height < 1) {
|
|
93
|
+
throw new Error("automationManifest viewport requires positive integer width and height.");
|
|
94
|
+
}
|
|
95
|
+
if (!Array.isArray(manifest.assertionIds) || manifest.assertionIds.length === 0) {
|
|
96
|
+
throw new Error("automationManifest requires assertionIds.");
|
|
97
|
+
}
|
|
98
|
+
const assertionIds = manifest.assertionIds.map((value) => safeIdentifier(value, "assertion id"));
|
|
99
|
+
if (new Set(assertionIds).size !== assertionIds.length) {
|
|
100
|
+
throw new Error("automationManifest assertionIds must be unique.");
|
|
101
|
+
}
|
|
102
|
+
assertAllowedKeys(manifest.consolePolicy, "automationManifest consolePolicy", [
|
|
103
|
+
"maxErrors",
|
|
104
|
+
"maxWarnings",
|
|
105
|
+
"maxExceptions",
|
|
106
|
+
]);
|
|
107
|
+
validateNonNegativeInteger(manifest.consolePolicy.maxErrors, "consolePolicy maxErrors");
|
|
108
|
+
validateNonNegativeInteger(manifest.consolePolicy.maxWarnings, "consolePolicy maxWarnings");
|
|
109
|
+
validateNonNegativeInteger(manifest.consolePolicy.maxExceptions, "consolePolicy maxExceptions");
|
|
110
|
+
if (manifest.externalNavigationPolicy !== "deny") {
|
|
111
|
+
throw new Error("visual-smoke MVP requires externalNavigationPolicy deny.");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function validateRequest(request) {
|
|
116
|
+
assertAllowedKeys(request, "Visual smoke request", REQUEST_KEYS);
|
|
117
|
+
if (request.schemaVersion !== "OwlCodaRunKitVisualSmokeRequestV1") {
|
|
118
|
+
throw new Error("Unsupported visual smoke request schemaVersion.");
|
|
119
|
+
}
|
|
120
|
+
safeIdentifier(request.smokeId, "smokeId");
|
|
121
|
+
if (!new Set(["project", "foreign_readonly"]).has(request.mode)) {
|
|
122
|
+
throw new Error("Visual smoke mode must be project or foreign_readonly.");
|
|
123
|
+
}
|
|
124
|
+
if (typeof request.targetRoot !== "string" || request.targetRoot.length === 0) {
|
|
125
|
+
throw new Error("Visual smoke request requires targetRoot.");
|
|
126
|
+
}
|
|
127
|
+
safeRelativePath(request.cwd, "Visual smoke cwd", { allowDot: true });
|
|
128
|
+
if (typeof request.executable !== "string" || !path.isAbsolute(request.executable)) {
|
|
129
|
+
throw new Error("Visual smoke requires an absolute executable path.");
|
|
130
|
+
}
|
|
131
|
+
if (!Array.isArray(request.argv)
|
|
132
|
+
|| request.argv.length === 0
|
|
133
|
+
|| request.argv.some((value) => typeof value !== "string" || value.length === 0)) {
|
|
134
|
+
throw new Error("Visual smoke requires exact non-empty argv.");
|
|
135
|
+
}
|
|
136
|
+
if (typeof request.launcherVersion !== "string" || request.launcherVersion.length === 0) {
|
|
137
|
+
throw new Error("Visual smoke requires launcherVersion.");
|
|
138
|
+
}
|
|
139
|
+
if (!Array.isArray(request.toolVersions)
|
|
140
|
+
|| request.toolVersions.some((entry) => !isRecord(entry)
|
|
141
|
+
|| typeof entry.name !== "string"
|
|
142
|
+
|| typeof entry.version !== "string")) {
|
|
143
|
+
throw new Error("Visual smoke toolVersions must contain name and version.");
|
|
144
|
+
}
|
|
145
|
+
if (!Array.isArray(request.selectedPaths) || request.selectedPaths.length === 0) {
|
|
146
|
+
throw new Error("Visual smoke requires selectedPaths.");
|
|
147
|
+
}
|
|
148
|
+
if (request.statusMode !== STATUS_MODE) {
|
|
149
|
+
throw new Error(`Visual smoke statusMode must be ${STATUS_MODE}.`);
|
|
150
|
+
}
|
|
151
|
+
safeRelativePath(request.resultPath, "Visual smoke resultPath");
|
|
152
|
+
const context = validateVerificationContext(request.verificationContext);
|
|
153
|
+
if (!context.valid) throw new Error(`Visual smoke verification context is invalid: ${context.issues.join("; ")}`);
|
|
154
|
+
validateAutomationManifest(request.automationManifest);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function resultIssues(result, manifest) {
|
|
158
|
+
const issues = [];
|
|
159
|
+
try {
|
|
160
|
+
assertAllowedKeys(result, "Visual smoke result", [
|
|
161
|
+
"schemaVersion",
|
|
162
|
+
"status",
|
|
163
|
+
"viewport",
|
|
164
|
+
"assertions",
|
|
165
|
+
"console",
|
|
166
|
+
"navigations",
|
|
167
|
+
"outputArtifacts",
|
|
168
|
+
]);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
return [error instanceof Error ? error.message : String(error)];
|
|
171
|
+
}
|
|
172
|
+
if (result.schemaVersion !== "OwlCodaRunKitVisualSmokeResultV1") {
|
|
173
|
+
issues.push("Unsupported visual smoke result schemaVersion.");
|
|
174
|
+
}
|
|
175
|
+
if (!new Set(["passed", "failed"]).has(result.status)) issues.push("Visual smoke result status must be passed or failed.");
|
|
176
|
+
if (!isRecord(result.viewport)
|
|
177
|
+
|| result.viewport.width !== manifest.viewport.width
|
|
178
|
+
|| result.viewport.height !== manifest.viewport.height) {
|
|
179
|
+
issues.push("Visual smoke viewport does not match the automation manifest.");
|
|
180
|
+
}
|
|
181
|
+
if (!Array.isArray(result.assertions)) {
|
|
182
|
+
issues.push("Visual smoke result requires assertions.");
|
|
183
|
+
} else {
|
|
184
|
+
const actualIds = [];
|
|
185
|
+
for (const assertion of result.assertions) {
|
|
186
|
+
if (!isRecord(assertion)
|
|
187
|
+
|| typeof assertion.id !== "string"
|
|
188
|
+
|| typeof assertion.passed !== "boolean"
|
|
189
|
+
|| Object.keys(assertion).some((key) => !new Set(["id", "passed"]).has(key))) {
|
|
190
|
+
issues.push("Visual smoke assertions require only id and passed.");
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
actualIds.push(assertion.id);
|
|
194
|
+
if (!assertion.passed) issues.push(`Visual assertion failed: ${assertion.id}`);
|
|
195
|
+
}
|
|
196
|
+
if (JSON.stringify([...actualIds].sort()) !== JSON.stringify([...manifest.assertionIds].sort())) {
|
|
197
|
+
issues.push("Visual smoke assertion ids do not match the automation manifest.");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (!isRecord(result.console)) {
|
|
201
|
+
issues.push("Visual smoke result requires console counts.");
|
|
202
|
+
} else {
|
|
203
|
+
for (const [field, policyField] of [
|
|
204
|
+
["errors", "maxErrors"],
|
|
205
|
+
["warnings", "maxWarnings"],
|
|
206
|
+
["exceptions", "maxExceptions"],
|
|
207
|
+
]) {
|
|
208
|
+
if (!Number.isInteger(result.console[field]) || result.console[field] < 0) {
|
|
209
|
+
issues.push(`Visual smoke console ${field} must be a non-negative integer.`);
|
|
210
|
+
} else if (result.console[field] > manifest.consolePolicy[policyField]) {
|
|
211
|
+
issues.push(`Visual smoke console ${field} exceeded policy.`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (!Array.isArray(result.navigations)) {
|
|
216
|
+
issues.push("Visual smoke result requires navigations.");
|
|
217
|
+
} else {
|
|
218
|
+
for (const navigation of result.navigations) {
|
|
219
|
+
if (!isRecord(navigation)
|
|
220
|
+
|| typeof navigation.protocol !== "string"
|
|
221
|
+
|| typeof navigation.external !== "boolean"
|
|
222
|
+
|| Object.keys(navigation).some((key) => !new Set(["protocol", "external"]).has(key))) {
|
|
223
|
+
issues.push("Visual smoke navigations require only protocol and external.");
|
|
224
|
+
} else if (navigation.external) {
|
|
225
|
+
issues.push("External navigation is forbidden by the automation manifest.");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (!Array.isArray(result.outputArtifacts) || result.outputArtifacts.length === 0) {
|
|
230
|
+
issues.push("Visual smoke result requires output artifacts.");
|
|
231
|
+
}
|
|
232
|
+
if (result.status !== "passed") issues.push("Visual smoke runner reported failed status.");
|
|
233
|
+
return issues;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function collectRunnerArtifacts({ workspaceRoot, evidenceRoot, result }) {
|
|
237
|
+
const issues = [];
|
|
238
|
+
const artifacts = [];
|
|
239
|
+
const seen = new Set();
|
|
240
|
+
if (!Array.isArray(result?.outputArtifacts)) return { artifacts, issues };
|
|
241
|
+
for (const entry of result.outputArtifacts) {
|
|
242
|
+
if (!isRecord(entry)
|
|
243
|
+
|| typeof entry.path !== "string"
|
|
244
|
+
|| typeof entry.mediaType !== "string"
|
|
245
|
+
|| entry.mediaType.length === 0
|
|
246
|
+
|| Object.keys(entry).some((key) => !new Set(["path", "mediaType"]).has(key))) {
|
|
247
|
+
issues.push("Visual smoke output artifacts require only path and mediaType.");
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
const safePath = safeRelativePath(entry.path, "Visual smoke output artifact path");
|
|
252
|
+
if (seen.has(safePath)) throw new Error(`Duplicate visual smoke output artifact: ${safePath}`);
|
|
253
|
+
seen.add(safePath);
|
|
254
|
+
const absolutePath = path.resolve(workspaceRoot, safePath);
|
|
255
|
+
if (!withinRoot(evidenceRoot, absolutePath)) {
|
|
256
|
+
throw new Error(`Visual smoke output artifact escapes the evidence root: ${safePath}`);
|
|
257
|
+
}
|
|
258
|
+
const stat = lstatSync(absolutePath);
|
|
259
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
260
|
+
throw new Error(`Visual smoke output artifact must be a regular non-symlink file: ${safePath}`);
|
|
261
|
+
}
|
|
262
|
+
artifacts.push({ path: safePath, sha256: sha256(readFileSync(absolutePath)) });
|
|
263
|
+
} catch (error) {
|
|
264
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
265
|
+
issues.push(`Visual smoke output artifact is invalid: ${message}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return { artifacts, issues };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function artifactEntry(workspaceRoot, absolutePath) {
|
|
272
|
+
return {
|
|
273
|
+
path: relativeToWorkspace(workspaceRoot, absolutePath),
|
|
274
|
+
sha256: sha256(readFileSync(absolutePath)),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function runVisualSmoke({ workspaceRoot, runId, request }) {
|
|
279
|
+
validateRequest(request);
|
|
280
|
+
const { executionRoot, pinGate } = loadActiveExecution(workspaceRoot, runId);
|
|
281
|
+
if (pinGate.status !== "valid") return pinGate;
|
|
282
|
+
const targetRoot = realpathSync(request.targetRoot);
|
|
283
|
+
if (request.mode === "project" && targetRoot !== workspaceRoot) {
|
|
284
|
+
throw new Error("Project visual smoke targetRoot must equal the RunKit workspace.");
|
|
285
|
+
}
|
|
286
|
+
const cwd = request.cwd === "."
|
|
287
|
+
? targetRoot
|
|
288
|
+
: realpathSync(resolveWithinRoot(targetRoot, request.cwd, "Visual smoke cwd"));
|
|
289
|
+
const smokeRoot = path.join(executionRoot, "visual-smoke-evidence", request.smokeId);
|
|
290
|
+
if (existsSync(smokeRoot)) throw new Error(`Visual smoke evidence already exists: ${request.smokeId}`);
|
|
291
|
+
const resultPath = resolveWithinRoot(workspaceRoot, request.resultPath, "Visual smoke resultPath");
|
|
292
|
+
const expectedResultPath = path.join(smokeRoot, "result.json");
|
|
293
|
+
if (resultPath !== expectedResultPath) {
|
|
294
|
+
throw new Error(`Visual smoke resultPath must be ${relativeToWorkspace(workspaceRoot, expectedResultPath)}.`);
|
|
295
|
+
}
|
|
296
|
+
mkdirSync(smokeRoot, { recursive: true });
|
|
297
|
+
const manifestPath = path.join(smokeRoot, "automation-manifest.json");
|
|
298
|
+
const stdoutPath = path.join(smokeRoot, "runner.stdout");
|
|
299
|
+
const stderrPath = path.join(smokeRoot, "runner.stderr");
|
|
300
|
+
const manifestBytes = Buffer.from(canonicalJson(request.automationManifest), "utf8");
|
|
301
|
+
writeFileSync(manifestPath, manifestBytes, { flag: "wx" });
|
|
302
|
+
const before = captureTargetState({ targetRoot, selectedPaths: request.selectedPaths });
|
|
303
|
+
const startedAt = new Date().toISOString();
|
|
304
|
+
const startedMs = Date.now();
|
|
305
|
+
const completed = spawnSync(request.executable, request.argv, {
|
|
306
|
+
cwd,
|
|
307
|
+
encoding: null,
|
|
308
|
+
env: process.env,
|
|
309
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
310
|
+
});
|
|
311
|
+
const endedAt = new Date().toISOString();
|
|
312
|
+
const stdout = completed.stdout ?? Buffer.alloc(0);
|
|
313
|
+
const stderr = completed.stderr ?? Buffer.from(completed.error?.message ?? "", "utf8");
|
|
314
|
+
writeFileSync(stdoutPath, stdout, { flag: "wx" });
|
|
315
|
+
writeFileSync(stderrPath, stderr, { flag: "wx" });
|
|
316
|
+
const after = captureTargetState({ targetRoot, selectedPaths: request.selectedPaths });
|
|
317
|
+
const stable = targetStable(before, after);
|
|
318
|
+
const issues = [];
|
|
319
|
+
let result = null;
|
|
320
|
+
let resultIsRegularFile = false;
|
|
321
|
+
if (!existsSync(resultPath)) {
|
|
322
|
+
issues.push("Visual smoke result JSON is missing.");
|
|
323
|
+
} else {
|
|
324
|
+
const resultStat = lstatSync(resultPath);
|
|
325
|
+
if (!resultStat.isFile() || resultStat.isSymbolicLink()) {
|
|
326
|
+
issues.push("Visual smoke result must be a regular non-symlink file.");
|
|
327
|
+
} else {
|
|
328
|
+
resultIsRegularFile = true;
|
|
329
|
+
try {
|
|
330
|
+
result = JSON.parse(readFileSync(resultPath, "utf8"));
|
|
331
|
+
issues.push(...resultIssues(result, request.automationManifest));
|
|
332
|
+
} catch {
|
|
333
|
+
issues.push("Visual smoke result must contain valid JSON.");
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const runnerArtifacts = collectRunnerArtifacts({ workspaceRoot, evidenceRoot: smokeRoot, result });
|
|
338
|
+
issues.push(...runnerArtifacts.issues);
|
|
339
|
+
if (sha256(readFileSync(manifestPath)) !== sha256(manifestBytes)) {
|
|
340
|
+
issues.push("Automation manifest changed while the runner executed.");
|
|
341
|
+
}
|
|
342
|
+
const runnerExitCode = Number.isInteger(completed.status) ? completed.status : 1;
|
|
343
|
+
if (runnerExitCode !== 0) issues.push(`Visual smoke runner exited with code ${runnerExitCode}.`);
|
|
344
|
+
const snapshotStatus = !stable
|
|
345
|
+
? "invalidated_by_target_write"
|
|
346
|
+
: issues.length === 0
|
|
347
|
+
? "snapshot_passed"
|
|
348
|
+
: "snapshot_failed";
|
|
349
|
+
const coreArtifacts = [manifestPath, stdoutPath, stderrPath];
|
|
350
|
+
if (resultIsRegularFile) coreArtifacts.push(resultPath);
|
|
351
|
+
const outputArtifacts = [
|
|
352
|
+
...coreArtifacts.map((absolutePath) => artifactEntry(workspaceRoot, absolutePath)),
|
|
353
|
+
...runnerArtifacts.artifacts,
|
|
354
|
+
].sort((left, right) => left.path.localeCompare(right.path));
|
|
355
|
+
const snapshotPath = path.join(executionRoot, "snapshots", `${request.smokeId}.json`);
|
|
356
|
+
const snapshot = {
|
|
357
|
+
schemaVersion: "OwlCodaRunKitSnapshotV1",
|
|
358
|
+
runId,
|
|
359
|
+
snapshotId: request.smokeId,
|
|
360
|
+
status: snapshotStatus,
|
|
361
|
+
mode: request.mode,
|
|
362
|
+
targetRoot,
|
|
363
|
+
verificationContextFingerprint: verificationContextFingerprint(request.verificationContext),
|
|
364
|
+
repositoryBefore: before,
|
|
365
|
+
repositoryAfter: after,
|
|
366
|
+
command: {
|
|
367
|
+
startedAt,
|
|
368
|
+
endedAt,
|
|
369
|
+
durationMs: Date.now() - startedMs,
|
|
370
|
+
exitCode: runnerExitCode,
|
|
371
|
+
stdoutPath: relativeToWorkspace(workspaceRoot, stdoutPath),
|
|
372
|
+
stdoutSha256: sha256(stdout),
|
|
373
|
+
stdoutLineCount: lineCount(stdout),
|
|
374
|
+
stderrPath: relativeToWorkspace(workspaceRoot, stderrPath),
|
|
375
|
+
stderrSha256: sha256(stderr),
|
|
376
|
+
stderrLineCount: lineCount(stderr),
|
|
377
|
+
evidence: {
|
|
378
|
+
schemaVersion: "OwlCodaRunKitReplayableEvidenceV1",
|
|
379
|
+
kind: "automation",
|
|
380
|
+
cwd: request.cwd,
|
|
381
|
+
launcher: { executable: request.executable, version: request.launcherVersion },
|
|
382
|
+
argv: [request.executable, ...request.argv],
|
|
383
|
+
automationManifestSha256: sha256(manifestBytes),
|
|
384
|
+
toolVersions: structuredClone(request.toolVersions),
|
|
385
|
+
materialInputs: Object.entries(before.selectedFiles).map(([id, hash]) => ({ id, sha256: hash })),
|
|
386
|
+
outputArtifacts,
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
visual: {
|
|
390
|
+
automationManifestPath: relativeToWorkspace(workspaceRoot, manifestPath),
|
|
391
|
+
resultPath: relativeToWorkspace(workspaceRoot, resultPath),
|
|
392
|
+
result,
|
|
393
|
+
issues,
|
|
394
|
+
},
|
|
395
|
+
authorizationGranted: false,
|
|
396
|
+
};
|
|
397
|
+
writeJsonExclusive(snapshotPath, snapshot);
|
|
398
|
+
return {
|
|
399
|
+
status: snapshotStatus === "snapshot_passed"
|
|
400
|
+
? "visual_smoke_passed"
|
|
401
|
+
: snapshotStatus === "invalidated_by_target_write"
|
|
402
|
+
? "invalidated_by_target_write"
|
|
403
|
+
: "visual_smoke_failed",
|
|
404
|
+
exitCode: snapshotStatus === "snapshot_passed" ? 0 : 2,
|
|
405
|
+
runId,
|
|
406
|
+
snapshotPath: relativeToWorkspace(workspaceRoot, snapshotPath),
|
|
407
|
+
issues,
|
|
408
|
+
authorizationGranted: false,
|
|
409
|
+
};
|
|
410
|
+
}
|