test(handoff): preserve native results in bounded attachment projections
This commit is contained in:
@@ -21,7 +21,7 @@ The controller snapshot also has no explicit protected candidate-scope checkpoin
|
|||||||
|
|
||||||
## Portable evidence
|
## Portable evidence
|
||||||
|
|
||||||
Native test reports, process observations and source manifests are stored as individually compressed `.gz` files under `evidence/` with exact hashes in TEST_EVIDENCE.json. Native XML/JSON contents are preserved, not rewritten to improve results. The local raw `.artifacts/automation-handoff-01/` evidence remains separately available; large videos, traces, model weights and document corpora are not imported into this package. Visual review uses its separately indexed synthetic comparison package.
|
Native test reports, process observations and source manifests are stored as individually compressed `.gz` files under `evidence/` with exact hashes in TEST_EVIDENCE.json. XML, Jest and ordinary metadata bytes are preserved. Playwright reports with inline attachment bodies use an explicitly labelled transport projection: only `results[].attachments[].body` is externalized. Native cases, counters, errors, retries, statuses, labels and identities remain intact; an indexed audit retains original report/body hashes, sizes, native statistics and original producer paths. This avoids committing roughly77MiB of compressed inline screenshots. The original raw reports remain untouched under `.artifacts/automation-handoff-01/`; no failed result is removed or improved. Importing consumers must recognise this projection and obtain the original raw artifact if their policy requires complete inline bodies. Large videos, traces and model weights are not imported. Actual synthetic PDFs/previews are supplied in the separate visual comparison package.
|
||||||
|
|
||||||
The package verifier only reads bounded local files/Git and optionally verifies the configured remote. It does not execute report commands, install dependencies, run a controller, inspect protected approval keys or grant readiness. Installed pinned Ajv/jsdom from the existing frontend dev dependency tree provide actual JSON Schema/XML validation; no dependency was added. External consumers may use their own qualified parsers and must not trust this model-writable verifier as their security boundary.
|
The package verifier only reads bounded local files/Git and optionally verifies the configured remote. It does not execute report commands, install dependencies, run a controller, inspect protected approval keys or grant readiness. Installed pinned Ajv/jsdom from the existing frontend dev dependency tree provide actual JSON Schema/XML validation; no dependency was added. External consumers may use their own qualified parsers and must not trust this model-writable verifier as their security boundary.
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,8 @@
|
|||||||
},
|
},
|
||||||
"artifact": {
|
"artifact": {
|
||||||
"type": "object", "additionalProperties": false, "required": ["id", "path", "sha256", "description"],
|
"type": "object", "additionalProperties": false, "required": ["id", "path", "sha256", "description"],
|
||||||
"properties": { "id": { "type": "string" }, "path": { "type": "string", "minLength": 1 }, "sha256": { "$ref": "#/definitions/sha" }, "description": { "type": "string" } }
|
"properties": { "id": { "type": "string" }, "path": { "type": "string", "minLength": 1 }, "sha256": { "$ref": "#/definitions/sha" }, "description": { "type": "string" }, "projection": { "const": "PLAYWRIGHT_INLINE_ATTACHMENT_BODIES_EXTERNALIZED" }, "sourceSha256": { "$ref": "#/definitions/sha" }, "sourceRelativePath": { "type": "string" }, "externalizedAttachmentCount": { "type": "integer", "minimum": 1 }, "projectionAuditArtifactId": { "type": "string" } },
|
||||||
|
"dependencies": { "projection": ["sourceSha256", "sourceRelativePath", "externalizedAttachmentCount", "projectionAuditArtifactId"] }
|
||||||
},
|
},
|
||||||
"report": {
|
"report": {
|
||||||
"type": "object", "additionalProperties": false,
|
"type": "object", "additionalProperties": false,
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ import { fileURLToPath } from 'node:url';
|
|||||||
import { validatePackage } from './verify.mjs';
|
import { validatePackage } from './verify.mjs';
|
||||||
|
|
||||||
const directory = path.dirname(fileURLToPath(import.meta.url));
|
const directory = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const artifactRoot = path.resolve(directory, '../../../../.artifacts');
|
const repository = fs.realpathSync(path.resolve(directory, '../../../..'));
|
||||||
|
const artifactRoot = path.join(repository, '.artifacts');
|
||||||
|
fs.mkdirSync(artifactRoot, { recursive: true });
|
||||||
|
assert.ok(fs.realpathSync(artifactRoot).startsWith(repository + path.sep));
|
||||||
const hash = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
|
const hash = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
|
||||||
function shadow(change) {
|
function shadow(change) {
|
||||||
const temporary = fs.mkdtempSync(path.join(artifactRoot, 'handoff-verify-'));
|
const temporary = fs.mkdtempSync(path.join(artifactRoot, 'handoff-verify-'));
|
||||||
@@ -17,7 +20,7 @@ function shadow(change) {
|
|||||||
change(temporary);
|
change(temporary);
|
||||||
assert.throws(() => validatePackage(temporary));
|
assert.throws(() => validatePackage(temporary));
|
||||||
} finally {
|
} finally {
|
||||||
const resolved = path.resolve(temporary);
|
const resolved = fs.realpathSync(temporary);
|
||||||
assert.ok(resolved.startsWith(artifactRoot + path.sep) && path.basename(resolved).startsWith('handoff-verify-'));
|
assert.ok(resolved.startsWith(artifactRoot + path.sep) && path.basename(resolved).startsWith('handoff-verify-'));
|
||||||
fs.rmSync(resolved, { recursive: true });
|
fs.rmSync(resolved, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,6 +133,12 @@ export function validatePackage(base = directory) {
|
|||||||
const bytes = readPackage(item.path);
|
const bytes = readPackage(item.path);
|
||||||
return item.path.endsWith('.gz') ? zlib.gunzipSync(bytes, { maxOutputLength: 8 * 1024 * 1024 }) : bytes;
|
return item.path.endsWith('.gz') ? zlib.gunzipSync(bytes, { maxOutputLength: 8 * 1024 * 1024 }) : bytes;
|
||||||
};
|
};
|
||||||
|
for (const item of evidence.artifacts.filter(entry => entry.projection)) {
|
||||||
|
const audit = JSON.parse(artifact(item.projectionAuditArtifactId).toString('utf8'));
|
||||||
|
const bytes = artifact(item.id);
|
||||||
|
demand(audit.sourceSha256 === item.sourceSha256 && audit.projectedSha256 === digest(bytes) && audit.removedBodies.length === item.externalizedAttachmentCount, 'Attachment-body export audit differs');
|
||||||
|
demand(JSON.stringify(audit.sourceStats) === JSON.stringify(JSON.parse(bytes.toString('utf8')).stats), 'Native browser statistics changed during export');
|
||||||
|
}
|
||||||
for (const report of evidence.reports) {
|
for (const report of evidence.reports) {
|
||||||
const expectedCommit = report.bindingScope === 'HANDOFF_VERIFIER' ? evidence.verifierCommit : evidence.testedCommit;
|
const expectedCommit = report.bindingScope === 'HANDOFF_VERIFIER' ? evidence.verifierCommit : evidence.testedCommit;
|
||||||
demand(report.sourceCommit === expectedCommit, 'Report source correspondence missing');
|
demand(report.sourceCommit === expectedCommit, 'Report source correspondence missing');
|
||||||
|
|||||||
Reference in New Issue
Block a user