devcontour-harness 0.0.0-stage → 0.1.0-alpha.6
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 +183 -2
- package/docs/architecture.md +473 -0
- package/docs/compatibility-versions.md +56 -0
- package/docs/compatibility.md +192 -0
- package/docs/connectors.md +323 -0
- package/docs/contracts.md +603 -0
- package/docs/integrations/hindsight.md +148 -0
- package/docs/quickstart.md +182 -0
- package/docs/release-notes/0.1.0-alpha.6.md +238 -0
- package/docs/review-probes.md +120 -0
- package/docs/review-protocol.md +251 -0
- package/docs/toolchains.md +167 -0
- package/docs/turbo-starter.md +269 -0
- package/docs/upgrade.md +202 -0
- package/examples/README.md +27 -0
- package/examples/external-connector/README.md +19 -0
- package/examples/external-connector/connector.mjs +55 -0
- package/examples/files-only/README.md +28 -0
- package/examples/files-only/context/norms/rules.md +4 -0
- package/examples/files-only/graph.json +23 -0
- package/examples/files-only/policy.json +30 -0
- package/examples/one-vendor/README.md +19 -0
- package/examples/one-vendor/policy.json +27 -0
- package/examples/single-task/README.md +18 -0
- package/examples/single-task/graph.json +27 -0
- package/examples/single-task/policy-same-vendor.json +55 -0
- package/examples/single-task/policy.json +52 -0
- package/examples/single-task/src/.gitkeep +0 -0
- package/examples/single-task/test/run.js +19 -0
- package/examples/toolchains/README.md +32 -0
- package/examples/toolchains/go/bad/label/label.go +9 -0
- package/examples/toolchains/go/good/label/label.go +9 -0
- package/examples/toolchains/go/policy.json +79 -0
- package/examples/toolchains/go/repo/REQUIREMENTS.md +9 -0
- package/examples/toolchains/go/repo/_gitignore +1 -0
- package/examples/toolchains/go/repo/go.mod +5 -0
- package/examples/toolchains/go/repo/go.sum +2 -0
- package/examples/toolchains/go/repo/label/label.go +9 -0
- package/examples/toolchains/go/repo/label/label_test.go +27 -0
- package/examples/toolchains/go/repo/label/version.go +9 -0
- package/examples/toolchains/go/task.json +25 -0
- package/examples/toolchains/mixed-go/bad/backend/label/label.go +9 -0
- package/examples/toolchains/mixed-go/bad/frontend/src/label.js +4 -0
- package/examples/toolchains/mixed-go/good/backend/label/label.go +9 -0
- package/examples/toolchains/mixed-go/good/frontend/src/label.js +4 -0
- package/examples/toolchains/mixed-go/policy.json +126 -0
- package/examples/toolchains/mixed-go/repo/REQUIREMENTS.md +9 -0
- package/examples/toolchains/mixed-go/repo/_gitignore +6 -0
- package/examples/toolchains/mixed-go/repo/backend/go.mod +3 -0
- package/examples/toolchains/mixed-go/repo/backend/label/label.go +9 -0
- package/examples/toolchains/mixed-go/repo/backend/label/label_test.go +30 -0
- package/examples/toolchains/mixed-go/repo/contract/check.mjs +17 -0
- package/examples/toolchains/mixed-go/repo/contract/label.json +9 -0
- package/examples/toolchains/mixed-go/repo/frontend/package.json +15 -0
- package/examples/toolchains/mixed-go/repo/frontend/pnpm-lock.yaml +22 -0
- package/examples/toolchains/mixed-go/repo/frontend/src/label.js +4 -0
- package/examples/toolchains/mixed-go/repo/frontend/test/label.test.js +11 -0
- package/examples/toolchains/mixed-go/task.json +25 -0
- package/examples/toolchains/mixed-python/bad/backend/src/fx_backend/__init__.py +6 -0
- package/examples/toolchains/mixed-python/bad/frontend/src/label.js +4 -0
- package/examples/toolchains/mixed-python/good/backend/src/fx_backend/__init__.py +6 -0
- package/examples/toolchains/mixed-python/good/frontend/src/label.js +4 -0
- package/examples/toolchains/mixed-python/policy.json +121 -0
- package/examples/toolchains/mixed-python/repo/REQUIREMENTS.md +9 -0
- package/examples/toolchains/mixed-python/repo/_gitignore +6 -0
- package/examples/toolchains/mixed-python/repo/backend/.python-version +1 -0
- package/examples/toolchains/mixed-python/repo/backend/pyproject.toml +19 -0
- package/examples/toolchains/mixed-python/repo/backend/src/fx_backend/__init__.py +6 -0
- package/examples/toolchains/mixed-python/repo/backend/tests/test_label.py +11 -0
- package/examples/toolchains/mixed-python/repo/backend/uv.lock +108 -0
- package/examples/toolchains/mixed-python/repo/contract/check.mjs +17 -0
- package/examples/toolchains/mixed-python/repo/contract/label.json +9 -0
- package/examples/toolchains/mixed-python/repo/frontend/package.json +15 -0
- package/examples/toolchains/mixed-python/repo/frontend/pnpm-lock.yaml +22 -0
- package/examples/toolchains/mixed-python/repo/frontend/src/label.js +4 -0
- package/examples/toolchains/mixed-python/repo/frontend/test/label.test.js +11 -0
- package/examples/toolchains/mixed-python/task.json +25 -0
- package/examples/toolchains/npm/bad/src/label.js +4 -0
- package/examples/toolchains/npm/good/src/label.js +4 -0
- package/examples/toolchains/npm/policy.json +82 -0
- package/examples/toolchains/npm/repo/REQUIREMENTS.md +9 -0
- package/examples/toolchains/npm/repo/_gitignore +4 -0
- package/examples/toolchains/npm/repo/package-lock.json +22 -0
- package/examples/toolchains/npm/repo/package.json +13 -0
- package/examples/toolchains/npm/repo/src/label.js +4 -0
- package/examples/toolchains/npm/repo/test/label.test.js +13 -0
- package/examples/toolchains/npm/task.json +25 -0
- package/examples/toolchains/pnpm/bad/packages/label/src/label.js +4 -0
- package/examples/toolchains/pnpm/good/packages/label/src/label.js +4 -0
- package/examples/toolchains/pnpm/policy.json +85 -0
- package/examples/toolchains/pnpm/repo/REQUIREMENTS.md +9 -0
- package/examples/toolchains/pnpm/repo/_gitignore +4 -0
- package/examples/toolchains/pnpm/repo/package.json +10 -0
- package/examples/toolchains/pnpm/repo/packages/checker/check.mjs +5 -0
- package/examples/toolchains/pnpm/repo/packages/checker/package.json +9 -0
- package/examples/toolchains/pnpm/repo/packages/label/package.json +14 -0
- package/examples/toolchains/pnpm/repo/packages/label/src/label.js +4 -0
- package/examples/toolchains/pnpm/repo/packages/label/test/label.test.js +13 -0
- package/examples/toolchains/pnpm/repo/pnpm-lock.yaml +29 -0
- package/examples/toolchains/pnpm/repo/pnpm-workspace.yaml +2 -0
- package/examples/toolchains/pnpm/task.json +25 -0
- package/examples/toolchains/python/bad/src/fx_label/__init__.py +6 -0
- package/examples/toolchains/python/good/src/fx_label/__init__.py +6 -0
- package/examples/toolchains/python/policy.json +80 -0
- package/examples/toolchains/python/repo/.python-version +1 -0
- package/examples/toolchains/python/repo/REQUIREMENTS.md +9 -0
- package/examples/toolchains/python/repo/_gitignore +5 -0
- package/examples/toolchains/python/repo/pyproject.toml +19 -0
- package/examples/toolchains/python/repo/src/fx_label/__init__.py +6 -0
- package/examples/toolchains/python/repo/tests/test_label.py +13 -0
- package/examples/toolchains/python/repo/uv.lock +108 -0
- package/examples/toolchains/python/task.json +25 -0
- package/examples/toolchains/turbo/bad/packages/label/src/label.js +4 -0
- package/examples/toolchains/turbo/good/packages/label/src/label.js +4 -0
- package/examples/toolchains/turbo/policy.json +110 -0
- package/examples/toolchains/turbo/repo/REQUIREMENTS.md +9 -0
- package/examples/toolchains/turbo/repo/_gitignore +4 -0
- package/examples/toolchains/turbo/repo/package.json +13 -0
- package/examples/toolchains/turbo/repo/packages/checker/check.mjs +5 -0
- package/examples/toolchains/turbo/repo/packages/checker/package.json +9 -0
- package/examples/toolchains/turbo/repo/packages/label/package.json +14 -0
- package/examples/toolchains/turbo/repo/packages/label/src/label.js +4 -0
- package/examples/toolchains/turbo/repo/packages/label/test/label.test.js +13 -0
- package/examples/toolchains/turbo/repo/pnpm-lock.yaml +94 -0
- package/examples/toolchains/turbo/repo/pnpm-workspace.yaml +2 -0
- package/examples/toolchains/turbo/repo/turbo.json +7 -0
- package/examples/toolchains/turbo/task.json +25 -0
- package/examples/turbo-starter/README.md +16 -0
- package/examples/turbo-starter/changes/issued-at-consumer-only.patch +12 -0
- package/examples/turbo-starter/changes/issued-at.patch +130 -0
- package/examples/turbo-starter/cleanup.mjs +58 -0
- package/examples/turbo-starter/graph.json +32 -0
- package/examples/turbo-starter/harness/acceptance/db-isolation.test.mjs +97 -0
- package/examples/turbo-starter/harness/acceptance/env-probe.test.mjs +75 -0
- package/examples/turbo-starter/harness/acceptance/generation.test.mjs +54 -0
- package/examples/turbo-starter/harness/acceptance/issued-at.test.mjs +43 -0
- package/examples/turbo-starter/harness/acceptance/lifecycle-probe.test.mjs +47 -0
- package/examples/turbo-starter/harness/compose-bridge.mjs +593 -0
- package/examples/turbo-starter/harness/compose.acceptance.yml +23 -0
- package/examples/turbo-starter/harness/env/backend.env.template +7 -0
- package/examples/turbo-starter/harness/env/compose.env.template +6 -0
- package/examples/turbo-starter/policy.json +109 -0
- package/examples/turbo-starter/policy.lifecycle.json +114 -0
- package/examples/turbo-starter/setup.mjs +352 -0
- package/examples/two-vendors/policy.json +30 -0
- package/lib/agents/cli-adapter.d.ts +48 -0
- package/lib/agents/cli-adapter.js +386 -0
- package/lib/agents/inspect.d.ts +6 -0
- package/lib/agents/inspect.js +46 -0
- package/lib/agents/probe-server.d.ts +2 -0
- package/lib/agents/probe-server.js +13 -0
- package/lib/agents/review-probe.d.ts +295 -0
- package/lib/agents/review-probe.js +872 -0
- package/lib/agents/runtime-events.d.ts +11 -0
- package/lib/agents/runtime-events.js +21 -0
- package/lib/agents/schemas.d.ts +217 -0
- package/lib/agents/schemas.js +124 -0
- package/lib/agents/usage.d.ts +8 -0
- package/lib/agents/usage.js +83 -0
- package/lib/cli.d.ts +2 -0
- package/lib/cli.js +491 -0
- package/lib/connectors/files.d.ts +108 -0
- package/lib/connectors/files.js +133 -0
- package/lib/contracts/checks.d.ts +307 -0
- package/lib/contracts/checks.js +181 -0
- package/lib/contracts/cli.d.ts +3895 -0
- package/lib/contracts/cli.js +664 -0
- package/lib/contracts/context.d.ts +135 -0
- package/lib/contracts/context.js +129 -0
- package/lib/contracts/digest.d.ts +9 -0
- package/lib/contracts/digest.js +22 -0
- package/lib/contracts/errors.d.ts +81 -0
- package/lib/contracts/errors.js +70 -0
- package/lib/contracts/events.d.ts +827 -0
- package/lib/contracts/events.js +258 -0
- package/lib/contracts/execution.d.ts +3216 -0
- package/lib/contracts/execution.js +872 -0
- package/lib/contracts/failure.d.ts +67 -0
- package/lib/contracts/failure.js +127 -0
- package/lib/contracts/index.d.ts +18 -0
- package/lib/contracts/index.js +18 -0
- package/lib/contracts/policy.d.ts +1080 -0
- package/lib/contracts/policy.js +544 -0
- package/lib/contracts/ports.d.ts +417 -0
- package/lib/contracts/ports.js +1 -0
- package/lib/contracts/primitives.d.ts +63 -0
- package/lib/contracts/primitives.js +73 -0
- package/lib/contracts/profiles.d.ts +336 -0
- package/lib/contracts/profiles.js +109 -0
- package/lib/contracts/published.d.ts +3237 -0
- package/lib/contracts/published.js +34 -0
- package/lib/contracts/result.d.ts +1280 -0
- package/lib/contracts/result.js +311 -0
- package/lib/contracts/review.d.ts +441 -0
- package/lib/contracts/review.js +477 -0
- package/lib/contracts/toolchain.d.ts +96 -0
- package/lib/contracts/toolchain.js +89 -0
- package/lib/contracts/version.d.ts +17 -0
- package/lib/contracts/version.js +17 -0
- package/lib/contracts/work.d.ts +173 -0
- package/lib/contracts/work.js +178 -0
- package/lib/control/base-move.d.ts +44 -0
- package/lib/control/base-move.js +90 -0
- package/lib/control/doctor.d.ts +50 -0
- package/lib/control/doctor.js +113 -0
- package/lib/control/harness.d.ts +1125 -0
- package/lib/control/harness.js +741 -0
- package/lib/control/provenance.d.ts +17 -0
- package/lib/control/provenance.js +25 -0
- package/lib/control/recovery.d.ts +237 -0
- package/lib/control/recovery.js +236 -0
- package/lib/control/review.d.ts +62 -0
- package/lib/control/review.js +178 -0
- package/lib/control/workflow.d.ts +247 -0
- package/lib/control/workflow.js +259 -0
- package/lib/engine/budget.d.ts +345 -0
- package/lib/engine/budget.js +278 -0
- package/lib/engine/classify.d.ts +8 -0
- package/lib/engine/classify.js +32 -0
- package/lib/engine/engine.d.ts +1527 -0
- package/lib/engine/engine.js +2259 -0
- package/lib/engine/failure.d.ts +1 -0
- package/lib/engine/failure.js +3 -0
- package/lib/engine/joint-runner.d.ts +257 -0
- package/lib/engine/joint-runner.js +254 -0
- package/lib/engine/measured.d.ts +15 -0
- package/lib/engine/measured.js +82 -0
- package/lib/engine/prompt.d.ts +66 -0
- package/lib/engine/prompt.js +131 -0
- package/lib/engine/property-recovery.d.ts +50 -0
- package/lib/engine/property-recovery.js +171 -0
- package/lib/engine/repair.d.ts +33 -0
- package/lib/engine/repair.js +82 -0
- package/lib/engine/result.d.ts +9 -0
- package/lib/engine/result.js +218 -0
- package/lib/engine/reuse.d.ts +80 -0
- package/lib/engine/reuse.js +102 -0
- package/lib/engine/runner.d.ts +360 -0
- package/lib/engine/runner.js +1836 -0
- package/lib/engine/standalone.d.ts +6 -0
- package/lib/engine/standalone.js +48 -0
- package/lib/engine/usage.d.ts +87 -0
- package/lib/engine/usage.js +165 -0
- package/lib/environment/isolation.d.ts +110 -0
- package/lib/environment/isolation.js +238 -0
- package/lib/environment/lifecycle.d.ts +40 -0
- package/lib/environment/lifecycle.js +197 -0
- package/lib/environment/live-calls.d.ts +14 -0
- package/lib/environment/live-calls.js +73 -0
- package/lib/environment/os-tools.d.ts +16 -0
- package/lib/environment/os-tools.js +35 -0
- package/lib/environment/process.d.ts +32 -0
- package/lib/environment/process.js +369 -0
- package/lib/environment/redaction.d.ts +22 -0
- package/lib/environment/redaction.js +102 -0
- package/lib/environment/resources.d.ts +31 -0
- package/lib/environment/resources.js +120 -0
- package/lib/index.d.ts +65 -0
- package/lib/index.js +67 -0
- package/lib/integrations/connector/commands.d.ts +358 -0
- package/lib/integrations/connector/commands.js +428 -0
- package/lib/integrations/connector/config.d.ts +215 -0
- package/lib/integrations/connector/config.js +214 -0
- package/lib/integrations/connector/delivery.d.ts +107 -0
- package/lib/integrations/connector/delivery.js +591 -0
- package/lib/integrations/connector/fake.d.ts +291 -0
- package/lib/integrations/connector/fake.js +786 -0
- package/lib/integrations/connector/history.d.ts +124 -0
- package/lib/integrations/connector/history.js +497 -0
- package/lib/integrations/connector/index.d.ts +16 -0
- package/lib/integrations/connector/index.js +16 -0
- package/lib/integrations/connector/redact.d.ts +19 -0
- package/lib/integrations/connector/redact.js +91 -0
- package/lib/integrations/connector/registry.d.ts +15 -0
- package/lib/integrations/connector/registry.js +43 -0
- package/lib/integrations/connector/snapshot.d.ts +131 -0
- package/lib/integrations/connector/snapshot.js +422 -0
- package/lib/integrations/connector/source.d.ts +37 -0
- package/lib/integrations/connector/source.js +73 -0
- package/lib/integrations/connector/steps.d.ts +71 -0
- package/lib/integrations/connector/steps.js +188 -0
- package/lib/integrations/connector/store.d.ts +140 -0
- package/lib/integrations/connector/store.js +277 -0
- package/lib/integrations/connector/summary.d.ts +38 -0
- package/lib/integrations/connector/summary.js +59 -0
- package/lib/integrations/connector/testing-worker.d.ts +1 -0
- package/lib/integrations/connector/testing-worker.js +81 -0
- package/lib/integrations/connector/testing.d.ts +205 -0
- package/lib/integrations/connector/testing.js +1615 -0
- package/lib/integrations/connector/transport.d.ts +240 -0
- package/lib/integrations/connector/transport.js +28 -0
- package/lib/integrations/memory/context.d.ts +120 -0
- package/lib/integrations/memory/context.js +239 -0
- package/lib/integrations/memory/index.d.ts +8 -0
- package/lib/integrations/memory/index.js +8 -0
- package/lib/integrations/memory/provider.d.ts +79 -0
- package/lib/integrations/memory/provider.js +22 -0
- package/lib/integrations/memory/sink.d.ts +56 -0
- package/lib/integrations/memory/sink.js +116 -0
- package/lib/integrations/memory/testing.d.ts +44 -0
- package/lib/integrations/memory/testing.js +61 -0
- package/lib/integrations/relay-v1/index.d.ts +122 -0
- package/lib/integrations/relay-v1/index.js +860 -0
- package/lib/integrations/relay-v2/index.d.ts +145 -0
- package/lib/integrations/relay-v2/index.js +752 -0
- package/lib/integrations/relay-v2/process.d.ts +32 -0
- package/lib/integrations/relay-v2/process.js +112 -0
- package/lib/integrations/relay-v2/protocol.d.ts +172 -0
- package/lib/integrations/relay-v2/protocol.js +142 -0
- package/lib/package.d.ts +2 -0
- package/lib/package.js +3 -0
- package/lib/profiles/resolve.d.ts +190 -0
- package/lib/profiles/resolve.js +273 -0
- package/lib/storage/document.d.ts +1234 -0
- package/lib/storage/document.js +80 -0
- package/lib/storage/sqlite.d.ts +35 -0
- package/lib/storage/sqlite.js +260 -0
- package/lib/testing/index.d.ts +5 -0
- package/lib/testing/index.js +5 -0
- package/lib/testing/store-contract.d.ts +8 -0
- package/lib/testing/store-contract.js +167 -0
- package/lib/toolchains/index.d.ts +9 -0
- package/lib/toolchains/index.js +9 -0
- package/lib/toolchains/inspect.d.ts +105 -0
- package/lib/toolchains/inspect.js +831 -0
- package/lib/toolchains/manifests.d.ts +67 -0
- package/lib/toolchains/manifests.js +238 -0
- package/lib/toolchains/probe.d.ts +28 -0
- package/lib/toolchains/probe.js +54 -0
- package/lib/toolchains/recipes.d.ts +33 -0
- package/lib/toolchains/recipes.js +51 -0
- package/lib/validate.d.ts +36 -0
- package/lib/validate.js +175 -0
- package/lib/verification/acceptance.d.ts +284 -0
- package/lib/verification/acceptance.js +670 -0
- package/lib/verification/checks.d.ts +72 -0
- package/lib/verification/checks.js +190 -0
- package/lib/verification/confirm.d.ts +125 -0
- package/lib/verification/confirm.js +411 -0
- package/lib/verification/corpus.d.ts +1303 -0
- package/lib/verification/corpus.js +744 -0
- package/lib/verification/dependencies.d.ts +66 -0
- package/lib/verification/dependencies.js +144 -0
- package/lib/verification/gates.d.ts +160 -0
- package/lib/verification/gates.js +600 -0
- package/lib/verification/joint.d.ts +63 -0
- package/lib/verification/joint.js +121 -0
- package/lib/verification/proof.d.ts +54 -0
- package/lib/verification/proof.js +96 -0
- package/lib/verification/property.d.ts +86 -0
- package/lib/verification/property.js +214 -0
- package/lib/verification/requirements.d.ts +25 -0
- package/lib/verification/requirements.js +69 -0
- package/lib/verification/self-check.d.ts +29 -0
- package/lib/verification/self-check.js +99 -0
- package/lib/verification/spike.d.ts +79 -0
- package/lib/verification/spike.js +124 -0
- package/package.json +100 -4
- package/profiles/expo-mobile.json +15 -0
- package/profiles/go-api.json +15 -0
- package/profiles/go-gotestsum.json +21 -0
- package/profiles/mobile-maestro.json +29 -0
- package/profiles/nest-api.json +26 -0
- package/profiles/next-product.json +72 -0
- package/profiles/node-checks.json +77 -0
- package/profiles/npm-ci.json +22 -0
- package/profiles/pnpm-frozen.json +24 -0
- package/profiles/python-api.json +32 -0
- package/profiles/python-uv.json +26 -0
- package/profiles/react-native.json +50 -0
- package/profiles/react-vite-admin.json +71 -0
- package/schemas/context-snapshot.schema.json +231 -0
- package/schemas/contract-document.schema.json +385 -0
- package/schemas/execution-document.schema.json +3695 -0
- package/schemas/execution-policy.schema.json +2196 -0
- package/schemas/joint-verification.schema.json +617 -0
- package/schemas/result-envelope.schema.json +1652 -0
- package/schemas/review-record.schema.json +177 -0
- package/schemas/run-event.schema.json +2458 -0
- package/schemas/run-result.schema.json +1570 -0
- package/schemas/stack-profile.schema.json +535 -0
- package/schemas/work-graph.schema.json +381 -0
- package/schemas/work-order.schema.json +256 -0
- package/skills/harness/SKILL.md +64 -0
- package/templates/AGENTS.md +13 -0
- package/templates/controller-mode/AGENTS.controller.md +58 -0
- package/templates/controller-mode/README.md +44 -0
- package/templates/graph.json +22 -0
- package/templates/policy.json +28 -0
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir, mkdtemp, rm, realpath, lstat, chmod } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join, resolve, sep, delimiter, relative, dirname } from 'node:path';
|
|
4
|
+
import { accessSync, constants, existsSync } from 'node:fs';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { XMLParser, XMLValidator } from 'fast-xml-parser';
|
|
7
|
+
import { sha256, } from "../contracts/index.js";
|
|
8
|
+
import { command, git } from "../environment/process.js";
|
|
9
|
+
import { isolation, isolatedCommand, isolationSupport } from "../environment/isolation.js";
|
|
10
|
+
import { TaskFailure } from "../engine/failure.js";
|
|
11
|
+
import { redactDeep } from "../environment/redaction.js";
|
|
12
|
+
import { manifestDigest, propertyCoverage, propertyFailure, propertySeed, readPropertyReport, } from "./property.js";
|
|
13
|
+
/** Длинное имя testcase заменяется хешем, а не обрезается: обрезка дала бы ложные совпадения. */
|
|
14
|
+
export const TEST_ID_LIMIT = 1000;
|
|
15
|
+
export function portableTest(test) {
|
|
16
|
+
return test.id.length > TEST_ID_LIMIT
|
|
17
|
+
? { ...test, id: 'sha256:' + createHash('sha256').update(test.id).digest('hex'), opaque: true }
|
|
18
|
+
: test;
|
|
19
|
+
}
|
|
20
|
+
/** Предел манифеста: полный список тестов крупного проекта в состояние не кладётся. */
|
|
21
|
+
const MANIFEST_LIMIT = 2000;
|
|
22
|
+
/**
|
|
23
|
+
* Полный текст провала testcase: атрибут message или содержимое элемента.
|
|
24
|
+
* Первая строка выделяется после redaction: многострочный секрет, обрезанный
|
|
25
|
+
* до первой строки раньше, redactor уже не узнал бы.
|
|
26
|
+
*/
|
|
27
|
+
function failureText(value) {
|
|
28
|
+
const first = Array.isArray(value) ? value[0] : value;
|
|
29
|
+
if (typeof first === 'string')
|
|
30
|
+
return entities(first);
|
|
31
|
+
if (first && typeof first === 'object') {
|
|
32
|
+
const node = first;
|
|
33
|
+
const text = node['@_message'] ?? node['#text'];
|
|
34
|
+
if (typeof text === 'string')
|
|
35
|
+
return entities(text);
|
|
36
|
+
}
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Пять предопределённых сущностей XML. Разбор идёт без обработки сущностей
|
|
41
|
+
* (DOCTYPE запрещён), и сообщение с JSON показывалось как `"`.
|
|
42
|
+
*/
|
|
43
|
+
const entities = (text) => text.replace(/&(quot|apos|lt|gt|amp);/g, (_m, name) => ({ quot: '"', apos: "'", lt: '<', gt: '>', amp: '&' })[name]);
|
|
44
|
+
/**
|
|
45
|
+
* Сводка JUnit-отчёта.
|
|
46
|
+
*
|
|
47
|
+
* `redact` применяется к именам и сообщениям до любой обрезки: обрезанный
|
|
48
|
+
* секрет redactor уже не узнаёт, и в сводке оставался бы его префикс. Имя
|
|
49
|
+
* testcase тоже проходит redact — оно попадает в состояние, журнал и API.
|
|
50
|
+
* Изменённое так имя не совпадёт с названным критерием, и подтверждения
|
|
51
|
+
* не будет: для секрета в имени теста это правильный исход.
|
|
52
|
+
*/
|
|
53
|
+
/**
|
|
54
|
+
* Значение атрибута XML: парсер работает без обработки сущностей (DOCTYPE
|
|
55
|
+
* запрещён, внешних сущностей нет), поэтому предопределённые сущности и
|
|
56
|
+
* числовые ссылки раскрываются здесь — один раз, без рекурсии. Иначе testcase
|
|
57
|
+
* `a["b"]` сохранялся как `a["b"]` и не совпадал с названным
|
|
58
|
+
* testId требования или критерия. Репортер JUnit Node.js экранирует `"`
|
|
59
|
+
* дважды (`&quot;`); оставшийся после раскрытия `"` поэтому тоже
|
|
60
|
+
* читается как `"`.
|
|
61
|
+
*/
|
|
62
|
+
export function xmlAttribute(value) {
|
|
63
|
+
const named = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" };
|
|
64
|
+
return value
|
|
65
|
+
.replace(/&(#x[0-9a-fA-F]+|#[0-9]+|amp|lt|gt|quot|apos);/g, (all, ref) => {
|
|
66
|
+
if (!ref.startsWith('#'))
|
|
67
|
+
return named[ref] ?? all;
|
|
68
|
+
const code = ref.startsWith('#x') ? parseInt(ref.slice(2), 16) : parseInt(ref.slice(1), 10);
|
|
69
|
+
return code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : all;
|
|
70
|
+
})
|
|
71
|
+
.replace(/"/g, '"');
|
|
72
|
+
}
|
|
73
|
+
export function junitSummary(xml, redact = (value) => value) {
|
|
74
|
+
if (xml.includes('<!DOCTYPE') || XMLValidator.validate(xml) !== true)
|
|
75
|
+
throw new Error('Некорректный JUnit XML');
|
|
76
|
+
const document = new XMLParser({
|
|
77
|
+
ignoreAttributes: false,
|
|
78
|
+
processEntities: false,
|
|
79
|
+
}).parse(xml);
|
|
80
|
+
const summary = {
|
|
81
|
+
tests: 0,
|
|
82
|
+
failures: 0,
|
|
83
|
+
skipped: 0,
|
|
84
|
+
failed: [],
|
|
85
|
+
failedDetails: [],
|
|
86
|
+
cases: [],
|
|
87
|
+
truncated: false,
|
|
88
|
+
};
|
|
89
|
+
function visit(value, key = '') {
|
|
90
|
+
if (Array.isArray(value)) {
|
|
91
|
+
value.forEach((v) => visit(v, key));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (key === 'testcase') {
|
|
95
|
+
summary.tests++;
|
|
96
|
+
if (value && typeof value === 'object') {
|
|
97
|
+
const named = value;
|
|
98
|
+
const raw = [named['@_classname'], named['@_name']]
|
|
99
|
+
.filter(Boolean)
|
|
100
|
+
.map((v) => xmlAttribute(String(v)))
|
|
101
|
+
.join('.');
|
|
102
|
+
const redacted = redact(raw);
|
|
103
|
+
// Длинное имя заменяется хешем: обрезка дала бы коллизии и ложные
|
|
104
|
+
// совпадения, а без замены receipt не перенёс бы манифест.
|
|
105
|
+
const name = portableTest({ id: redacted }).id;
|
|
106
|
+
const opaque = name !== raw;
|
|
107
|
+
const broken = 'failure' in value || 'error' in value;
|
|
108
|
+
if (broken) {
|
|
109
|
+
summary.failures++;
|
|
110
|
+
if (name && summary.failed.length < 10) {
|
|
111
|
+
summary.failed.push(String(name));
|
|
112
|
+
summary.failedDetails.push([
|
|
113
|
+
String(name),
|
|
114
|
+
redact(failureText(named['failure'] ?? named['error']))
|
|
115
|
+
.trim()
|
|
116
|
+
.split('\n')[0],
|
|
117
|
+
]
|
|
118
|
+
.filter(Boolean)
|
|
119
|
+
.join(': ')
|
|
120
|
+
.slice(0, 200));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if ('skipped' in value)
|
|
124
|
+
summary.skipped++;
|
|
125
|
+
if (name) {
|
|
126
|
+
if (summary.cases.length < MANIFEST_LIMIT)
|
|
127
|
+
summary.cases.push({
|
|
128
|
+
id: String(name),
|
|
129
|
+
status: broken ? 'failed' : 'skipped' in value ? 'skipped' : 'passed',
|
|
130
|
+
...(opaque ? { opaque: true } : {}),
|
|
131
|
+
});
|
|
132
|
+
else
|
|
133
|
+
summary.truncated = true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (value && typeof value === 'object')
|
|
139
|
+
for (const [k, v] of Object.entries(value))
|
|
140
|
+
visit(v, k);
|
|
141
|
+
}
|
|
142
|
+
visit(document);
|
|
143
|
+
if (!summary.tests)
|
|
144
|
+
throw new Error('JUnit не содержит выполненных testcases');
|
|
145
|
+
return summary;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Короткая выжимка вывода провалившейся проверки.
|
|
149
|
+
*
|
|
150
|
+
* Полный лог лежит в артефакте, но исполнителю следующей попытки он не
|
|
151
|
+
* доступен, а в отказ попадал только «Код выхода 1». Два разных провала одной
|
|
152
|
+
* проверки выглядели одной причиной — предел одинаковых повторов исчерпывался
|
|
153
|
+
* на несвязанных ошибках, и подсказка повтора не содержала ничего, кроме кода
|
|
154
|
+
* возврата. Берутся последние содержательные строки обоих потоков: ошибку
|
|
155
|
+
* одни инструменты пишут в stdout, другие в stderr, и одинаковое
|
|
156
|
+
* предупреждение в одном потоке не должно закрывать ошибку в другом. Секреты
|
|
157
|
+
* снимаются тем же redact, что и в логе; объём ограничен.
|
|
158
|
+
*/
|
|
159
|
+
function diagnostic(stderr, stdout, redact) {
|
|
160
|
+
// У каждого потока свой бюджет: общий лимит, заполняемый по очереди,
|
|
161
|
+
// позволял нескольким длинным строкам одного потока вытеснить ошибку
|
|
162
|
+
// другого. Redact — до обрезки, иначе префикс секрета остаётся.
|
|
163
|
+
const tail = (text) => {
|
|
164
|
+
const joined = text
|
|
165
|
+
.split('\n')
|
|
166
|
+
.map((line) => (redact ? redact(line) : line).trim())
|
|
167
|
+
.filter(Boolean)
|
|
168
|
+
.slice(-4)
|
|
169
|
+
.map((line) => (line.length > 140 ? line.slice(0, 140) + '…' : line))
|
|
170
|
+
.join(' | ');
|
|
171
|
+
return joined.length > 290 ? '…' + joined.slice(-290) : joined;
|
|
172
|
+
};
|
|
173
|
+
return [tail(stdout), tail(stderr)].filter(Boolean).join(' | ');
|
|
174
|
+
}
|
|
175
|
+
/** Упавшие testcases с их сообщениями — то, чем провалы отличаются друг от друга. */
|
|
176
|
+
function failedCases(counts) {
|
|
177
|
+
return counts.failedDetails.join('; ').slice(0, 600);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Песочница отказала до запуска команды: процесс завершился неуспешно, а
|
|
181
|
+
* метка старта, которую обёртка создаёт перед exec, не появилась. Текст
|
|
182
|
+
* stderr признаком не служит — его может напечатать и сама проверка.
|
|
183
|
+
*/
|
|
184
|
+
export function sandboxNotStarted(result, started) {
|
|
185
|
+
return started !== undefined && result.code !== 0 && !existsSync(started);
|
|
186
|
+
}
|
|
187
|
+
/** Команда проверки находится — иначе ENOENT, как при обычном запуске. */
|
|
188
|
+
function assertExecutable(name, path, cwd) {
|
|
189
|
+
const candidates = name.includes('/')
|
|
190
|
+
? [resolve(cwd, name)]
|
|
191
|
+
: (path ?? '')
|
|
192
|
+
.split(delimiter)
|
|
193
|
+
.filter(Boolean)
|
|
194
|
+
.map((dir) => join(dir, name));
|
|
195
|
+
if (!candidates.some((file) => {
|
|
196
|
+
try {
|
|
197
|
+
accessSync(file, constants.X_OK);
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}))
|
|
204
|
+
throw new Error(`spawn ${name} ENOENT`);
|
|
205
|
+
}
|
|
206
|
+
/** Отчёт внутри worktree, прочитанный без выхода за его границы. */
|
|
207
|
+
async function readReport(reportPath, cwd, redact) {
|
|
208
|
+
const real = await realpath(reportPath);
|
|
209
|
+
if (!real.startsWith((await realpath(cwd)) + sep))
|
|
210
|
+
throw new Error('Report symlink выходит из worktree');
|
|
211
|
+
const xml = await readFile(real, 'utf8');
|
|
212
|
+
return { ...junitSummary(xml, redact), xml };
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Путь отчёта внутри корня, созданный без выхода за него.
|
|
216
|
+
*
|
|
217
|
+
* Раньше каталоги создавались рекурсивно, а граница проверялась потом:
|
|
218
|
+
* symlink внутри worktree успевал увести создание каталога наружу до отказа.
|
|
219
|
+
* Теперь путь проходится от корня по одному компоненту: существующий symlink
|
|
220
|
+
* или не-каталог — отказ до любой записи, недостающий каталог создаётся
|
|
221
|
+
* только внутри уже проверенного родителя.
|
|
222
|
+
*/
|
|
223
|
+
export async function prepareReportPath(root, path, escape) {
|
|
224
|
+
const base = await realpath(root);
|
|
225
|
+
const target = resolve(base, path);
|
|
226
|
+
if (!target.startsWith(base + sep))
|
|
227
|
+
throw new Error(escape);
|
|
228
|
+
let current = base;
|
|
229
|
+
for (const part of relative(base, dirname(target)).split(sep).filter(Boolean)) {
|
|
230
|
+
current = join(current, part);
|
|
231
|
+
let stat = await lstat(current).catch(() => undefined);
|
|
232
|
+
if (!stat) {
|
|
233
|
+
await mkdir(current);
|
|
234
|
+
stat = await lstat(current);
|
|
235
|
+
}
|
|
236
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
237
|
+
throw new Error(escape);
|
|
238
|
+
}
|
|
239
|
+
await rm(target, { force: true });
|
|
240
|
+
return target;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Команда проверки — задачи или общей приёмки workspace — в песочнице ОС.
|
|
244
|
+
*
|
|
245
|
+
* Прежде проверка шла обычным процессом хоста и могла читать базу контура и
|
|
246
|
+
* соседние файлы, писать вне worktree и ходить в сеть: сверка HEAD и
|
|
247
|
+
* отслеживаемых файлов после неё этого не видит. Без механизма песочницы
|
|
248
|
+
* проверка не запускается — снять изоляцию можно только явным
|
|
249
|
+
* isolation.mode: none. Запись — в `write` и собственный scratch (TMPDIR).
|
|
250
|
+
*/
|
|
251
|
+
export async function runCheck(
|
|
252
|
+
// Нужна только политика изоляции: проверки ревьюера исполняются вне сервера.
|
|
253
|
+
h, options) {
|
|
254
|
+
let argv = options.argv, env = options.env, started;
|
|
255
|
+
const scratch = await realpath(await mkdtemp(join(tmpdir(), 'dc-gate-')));
|
|
256
|
+
// Метка старта — вне TMPDIR команды: проверка, чистящая свой временный
|
|
257
|
+
// каталог, не должна стирать признак того, что она уже начиналась.
|
|
258
|
+
const marks = await realpath(await mkdtemp(join(tmpdir(), 'dc-start-')));
|
|
259
|
+
try {
|
|
260
|
+
if (h.config.isolation.mode === 'os') {
|
|
261
|
+
const support = isolationBackend.check();
|
|
262
|
+
if (!support.ok)
|
|
263
|
+
throw new Error(`Изоляция проверок недоступна: ${support.detail}. Установите зависимости или явно задайте isolation.mode: none`);
|
|
264
|
+
// Отсутствующая команда внутри песочницы выглядела бы как «код 127»
|
|
265
|
+
// от обёртки. Причина та же, что и без неё, — называем её так же.
|
|
266
|
+
assertExecutable(argv[0], env.PATH, options.cwd);
|
|
267
|
+
const policy = isolation({
|
|
268
|
+
write: [...options.write, scratch, marks],
|
|
269
|
+
controller: options.controller,
|
|
270
|
+
readable: options.readable,
|
|
271
|
+
domains: h.config.isolation.domains,
|
|
272
|
+
});
|
|
273
|
+
started = join(marks, 'started');
|
|
274
|
+
const wrapped = await isolatedCommand(policy, argv, options.settingsDir, scratch, started);
|
|
275
|
+
argv = wrapped.argv;
|
|
276
|
+
env = { ...env, ...wrapped.env };
|
|
277
|
+
}
|
|
278
|
+
const execute = () => command(argv, options.cwd, {
|
|
279
|
+
signal: options.signal,
|
|
280
|
+
timeoutMs: options.timeoutMs,
|
|
281
|
+
env,
|
|
282
|
+
redact: options.redact,
|
|
283
|
+
// Отсоединившийся потомок проверки не переживает её: и при успехе,
|
|
284
|
+
// и по таймауту, отмене или падению.
|
|
285
|
+
// Каталоги проверки принадлежат только ей: процесс, запущенный в них
|
|
286
|
+
// во время проверки, — её потомок, даже без метки и родства.
|
|
287
|
+
contain: { dirs: [...options.write, scratch, marks] },
|
|
288
|
+
});
|
|
289
|
+
let result = await execute(), attempts = 1;
|
|
290
|
+
// sandbox-runtime ищет shell через `which` с таймаутом в секунду и под
|
|
291
|
+
// нагрузкой отказывает до запуска команды. Повтор допустим, только если
|
|
292
|
+
// команда точно не начиналась: метки старта нет.
|
|
293
|
+
for (; attempts < 3 && sandboxNotStarted(result, started); attempts++) {
|
|
294
|
+
await new Promise((r) => setTimeout(r, 500 * attempts));
|
|
295
|
+
result = await execute();
|
|
296
|
+
}
|
|
297
|
+
return { ...result, attempts };
|
|
298
|
+
}
|
|
299
|
+
finally {
|
|
300
|
+
// Каждый каталог убирается независимо: отказ уборки временного каталога
|
|
301
|
+
// проверки (она могла снять с него права) не должен оставлять и метки.
|
|
302
|
+
const removed = await Promise.allSettled([
|
|
303
|
+
rm(scratch, { recursive: true, force: true }),
|
|
304
|
+
rm(marks, { recursive: true, force: true }),
|
|
305
|
+
]);
|
|
306
|
+
const failed = removed.find((r) => r.status === 'rejected');
|
|
307
|
+
if (failed)
|
|
308
|
+
throw failed.reason;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/** Проверка механизма песочницы; тест подменяет её, чтобы проверить отказ. */
|
|
312
|
+
export const isolationBackend = { check: isolationSupport };
|
|
313
|
+
/**
|
|
314
|
+
* Хосты, в сети к которым проверке, по всей видимости, отказала песочница.
|
|
315
|
+
*
|
|
316
|
+
* Прокси песочницы отвечает на запрещённый домен `403`, но клиенты вроде
|
|
317
|
+
* pnpm показывают только «Forbidden - 403» и URL. На пилоте такой отказ
|
|
318
|
+
* записывался провалом проверки, и контур переписывал код, который был ни
|
|
319
|
+
* при чём. Признак узкий: в выводе есть отказ (403, Forbidden, прокси или
|
|
320
|
+
* тело ответа песочницы) и URL хоста, которого нет в `isolation.domains`.
|
|
321
|
+
*/
|
|
322
|
+
export function sandboxBlockedHosts(output, allowed) {
|
|
323
|
+
if (!/\b403\b|forbidden|denied by sandbox policy|blocked-by-sandbox-runtime|proxy|connect EPERM/i.test(output))
|
|
324
|
+
return [];
|
|
325
|
+
const hosts = new Set();
|
|
326
|
+
for (const m of output.matchAll(/\bhttps?:\/\/([a-z0-9-]+(?:\.[a-z0-9-]+)+)/gi))
|
|
327
|
+
hosts.add(m[1].toLowerCase());
|
|
328
|
+
return [...hosts].filter((host) => host !== '127.0.0.1' &&
|
|
329
|
+
!allowed.some((d) => host === d.toLowerCase() || host.endsWith('.' + d.toLowerCase())));
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Окружение свежести test gate: те же переменные у gate движка и у gate,
|
|
333
|
+
* запущенного проверкой reviewer, — иначе probe мог бы прочитать cache там,
|
|
334
|
+
* где gate исполняет тесты заново.
|
|
335
|
+
*/
|
|
336
|
+
export function freshnessEnv(kind, env) {
|
|
337
|
+
return kind === 'test'
|
|
338
|
+
? { TURBO_FORCE: 'true', GOFLAGS: [env.GOFLAGS, '-count=1'].filter(Boolean).join(' ') }
|
|
339
|
+
: {};
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Gate попытки. `test` принимается только по свежему (прежний отчёт удалён до
|
|
343
|
+
* запуска) непустому JUnit без failures, errors и skips на этом SHA; exit 0
|
|
344
|
+
* без отчёта — отказ. Gate, изменивший HEAD или отслеживаемые файлы, не
|
|
345
|
+
* проходит. Evidence записывается при любом исходе; провал — `TaskFailure`.
|
|
346
|
+
*/
|
|
347
|
+
export async function runGate(run) {
|
|
348
|
+
const { cwd, sha, phase, gate, artifactDir, signal } = run;
|
|
349
|
+
const redact = run.execution.redact;
|
|
350
|
+
// У проверки потребителя дерево — его голова, а `sha` — интегрированный
|
|
351
|
+
// результат провайдера: неизменной обязана остаться голова потребителя.
|
|
352
|
+
const expectedHead = run.consumer ? await git(cwd, 'rev-parse', 'HEAD') : sha;
|
|
353
|
+
const root = await realpath(cwd);
|
|
354
|
+
const gateCwd = gate.cwd ? await realpath(resolve(cwd, gate.cwd)) : root;
|
|
355
|
+
if (gateCwd !== root && !gateCwd.startsWith(root + sep))
|
|
356
|
+
throw new Error('Gate cwd выходит из worktree');
|
|
357
|
+
const reportPath = gate.report
|
|
358
|
+
? await prepareReportPath(cwd, gate.report.path, 'Report выходит из worktree')
|
|
359
|
+
: undefined;
|
|
360
|
+
// Генеративная проверка: seed задаёт controller, поэтому он известен и
|
|
361
|
+
// тогда, когда проверка зависла и не успела ничего записать. Отчёт свойств
|
|
362
|
+
// лежит в каталоге controller вне worktree: проверка пишет внутрь него, но
|
|
363
|
+
// заменить сам каталог на ссылку наружу не может.
|
|
364
|
+
const property = gate.property && run.property ? run.property : undefined;
|
|
365
|
+
const propertyDir = property
|
|
366
|
+
? await realpath(await mkdtemp(join(tmpdir(), 'harness-property-')))
|
|
367
|
+
: undefined;
|
|
368
|
+
const propertyPath = propertyDir ? join(propertyDir, 'report.json') : undefined;
|
|
369
|
+
const seed = property ? propertySeed(property.runId, phase, gate.id) : 0;
|
|
370
|
+
const expected = property?.expected;
|
|
371
|
+
// Корпус закреплён за попыткой при выдаче: его случаи материализуются из
|
|
372
|
+
// закреплённых байтов в каталоге controller вне worktree, только для чтения.
|
|
373
|
+
const corpus = expected?.corpus && property?.corpus ? property.corpus : undefined;
|
|
374
|
+
const corpusDir = corpus
|
|
375
|
+
? await realpath(await mkdtemp(join(tmpdir(), 'harness-corpus-')))
|
|
376
|
+
: undefined;
|
|
377
|
+
const corpusPath = corpusDir ? join(corpusDir, 'corpus.json') : undefined;
|
|
378
|
+
if (corpusPath) {
|
|
379
|
+
await writeFile(corpusPath, corpus.content);
|
|
380
|
+
await chmod(corpusPath, 0o444);
|
|
381
|
+
}
|
|
382
|
+
const propertyEnv = propertyPath
|
|
383
|
+
? {
|
|
384
|
+
DEVCONTOUR_SEED: String(seed),
|
|
385
|
+
DEVCONTOUR_PROPERTY_REPORT: propertyPath,
|
|
386
|
+
...(expected
|
|
387
|
+
? { DEVCONTOUR_PROPERTIES: expected.properties.map((p) => p.id).join(',') }
|
|
388
|
+
: {}),
|
|
389
|
+
...(corpusPath ? { DEVCONTOUR_CORPUS: corpusPath } : {}),
|
|
390
|
+
}
|
|
391
|
+
: {};
|
|
392
|
+
// Test gate исполняет тесты заново, через какую бы обёртку (package
|
|
393
|
+
// script, task runner) ни шла команда: Turbo не читает cache (`TURBO_FORCE`),
|
|
394
|
+
// Go не берёт результат из кэша тестов (`-count=1` в GOFLAGS, последним).
|
|
395
|
+
// Восстановленный из cache зелёный отчёт иначе выглядел бы свежим.
|
|
396
|
+
const freshness = freshnessEnv(gate.kind, run.execution.env);
|
|
397
|
+
let kind = 'gate';
|
|
398
|
+
// Заголовок лога — фактические каталог и argv запуска: digest лога входит в
|
|
399
|
+
// evidence, и запись называет, где и чем проверка исполнялась.
|
|
400
|
+
let passed = false, summary = '', log = `# harness gate ${gate.id}: cwd=${relative(root, gateCwd) || '.'} argv=${JSON.stringify(gate.command.map(redact))}${gate.kind === 'test' ? ` fresh=${redact(JSON.stringify(freshness))}` : ''}\n`, exitCode = -1, manifest;
|
|
401
|
+
try {
|
|
402
|
+
const result = await runCheck({ config: run.policy }, {
|
|
403
|
+
argv: gate.command,
|
|
404
|
+
cwd: gateCwd,
|
|
405
|
+
env: {
|
|
406
|
+
...run.execution.env,
|
|
407
|
+
...freshness,
|
|
408
|
+
...(reportPath
|
|
409
|
+
? { DEVCONTOUR_REPORT_PATH: reportPath, HARNESS_REPORT_PATH: reportPath }
|
|
410
|
+
: {}),
|
|
411
|
+
...propertyEnv,
|
|
412
|
+
...Object.fromEntries(Object.entries(propertyEnv).map(([k, v]) => [k.replace(/^DEVCONTOUR_/, 'HARNESS_'), v])),
|
|
413
|
+
},
|
|
414
|
+
redact,
|
|
415
|
+
timeoutMs: gate.timeoutMs,
|
|
416
|
+
signal,
|
|
417
|
+
write: [cwd, ...(propertyDir ? [propertyDir] : [])],
|
|
418
|
+
readable: [...run.boundary.readable, ...(corpusDir ? [corpusDir] : [])],
|
|
419
|
+
controller: run.boundary.hidden,
|
|
420
|
+
settingsDir: join(artifactDir, gate.id),
|
|
421
|
+
});
|
|
422
|
+
exitCode = result.code;
|
|
423
|
+
log += result.stdout + '\n' + result.stderr;
|
|
424
|
+
if (result.timedOut || signal.aborted)
|
|
425
|
+
throw new Error('Проверка прервана или превысила timeout');
|
|
426
|
+
if (result.code !== 0) {
|
|
427
|
+
const blocked = run.policy.isolation.mode === 'os'
|
|
428
|
+
? sandboxBlockedHosts(result.stdout + '\n' + result.stderr, run.policy.isolation.domains)
|
|
429
|
+
: [];
|
|
430
|
+
if (blocked.length) {
|
|
431
|
+
kind = 'environment';
|
|
432
|
+
throw new Error(`песочница проверки закрыла сеть к ${blocked.join(', ')}: этих доменов нет в isolation.domains. ` +
|
|
433
|
+
`Код выхода ${result.code}: ${diagnostic(result.stderr, result.stdout, redact)}`);
|
|
434
|
+
}
|
|
435
|
+
const counts = reportPath
|
|
436
|
+
? await readReport(reportPath, cwd, redact).catch(() => undefined)
|
|
437
|
+
: undefined;
|
|
438
|
+
if (counts)
|
|
439
|
+
manifest = counts;
|
|
440
|
+
throw new Error([
|
|
441
|
+
`Код выхода ${result.code}`,
|
|
442
|
+
counts?.failures ? `упали ${failedCases(counts)}` : '',
|
|
443
|
+
diagnostic(result.stderr, result.stdout, redact),
|
|
444
|
+
]
|
|
445
|
+
.filter(Boolean)
|
|
446
|
+
.join(': '));
|
|
447
|
+
}
|
|
448
|
+
if (reportPath) {
|
|
449
|
+
const counts = (manifest = await readReport(reportPath, cwd, redact));
|
|
450
|
+
summary = `${counts.tests} tests, ${counts.failures} failures, ${counts.skipped} skipped`;
|
|
451
|
+
if (counts.failures || counts.skipped)
|
|
452
|
+
throw new Error(summary + (counts.failures ? `: ${failedCases(counts)}` : ''));
|
|
453
|
+
await mkdir(artifactDir, { recursive: true });
|
|
454
|
+
await writeFile(join(artifactDir, `${gate.id}.xml`), redact(counts.xml));
|
|
455
|
+
}
|
|
456
|
+
else
|
|
457
|
+
summary = 'Команда завершилась успешно';
|
|
458
|
+
if ((await git(cwd, 'rev-parse', 'HEAD')) !== expectedHead)
|
|
459
|
+
throw new Error('Gate изменил HEAD');
|
|
460
|
+
if (await git(cwd, 'status', '--porcelain', '--untracked-files=no'))
|
|
461
|
+
throw new Error('Gate изменил отслеживаемые файлы');
|
|
462
|
+
passed = true;
|
|
463
|
+
}
|
|
464
|
+
catch (error) {
|
|
465
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
466
|
+
summary = redact(message);
|
|
467
|
+
log += '\n' + summary;
|
|
468
|
+
}
|
|
469
|
+
// Отчёт свойств читается при любом исходе: провал и таймаут — ровно те
|
|
470
|
+
// случаи, когда контрпример и вход нужны больше всего.
|
|
471
|
+
let propertyEvidence;
|
|
472
|
+
if (property && propertyPath) {
|
|
473
|
+
const read = await readPropertyReport(propertyPath, propertyDir, redact);
|
|
474
|
+
// Уборка каталога — не повод потерять evidence: проверка могла снять с
|
|
475
|
+
// него права. Права возвращаются; если убрать всё же не удалось, это
|
|
476
|
+
// отказ gate с причиной, а не исключение до записи доказательства.
|
|
477
|
+
let cleanup;
|
|
478
|
+
try {
|
|
479
|
+
await rm(propertyDir, { recursive: true, force: true });
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
try {
|
|
483
|
+
await chmod(propertyDir, 0o700);
|
|
484
|
+
await rm(propertyDir, { recursive: true, force: true });
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
cleanup =
|
|
488
|
+
'каталог отчёта свойств не убран: ' +
|
|
489
|
+
redact(error instanceof Error ? error.message : String(error));
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
const join2 = (text) => summary && summary !== 'Команда завершилась успешно' ? `${summary}; ${text}` : text;
|
|
493
|
+
if (cleanup) {
|
|
494
|
+
passed = false;
|
|
495
|
+
summary = join2(cleanup);
|
|
496
|
+
}
|
|
497
|
+
// Корпус исполнялся из закреплённых байтов: проверка без песочницы могла
|
|
498
|
+
// бы их переписать, и тогда свидетельство относилось бы к другим случаям.
|
|
499
|
+
if (corpusDir) {
|
|
500
|
+
const intact = await readFile(corpusPath, 'utf8')
|
|
501
|
+
.then((text) => text === corpus.content)
|
|
502
|
+
.catch(() => false);
|
|
503
|
+
await chmod(corpusPath, 0o600).catch(() => undefined);
|
|
504
|
+
await rm(corpusDir, { recursive: true, force: true }).catch(() => undefined);
|
|
505
|
+
if (!intact) {
|
|
506
|
+
passed = false;
|
|
507
|
+
summary = 'Проверка изменила файл закреплённого корпуса';
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
const failure = read.report ? propertyFailure(read.report) : '';
|
|
511
|
+
// Покрытие сверяется всегда, когда отчёт прочитан: зелёный код выхода при
|
|
512
|
+
// пустом отчёте, нуле входов или пропущенном свойстве не доказывает
|
|
513
|
+
// объявленного. Провал свойства уже назван — недобор при нём следствие.
|
|
514
|
+
const coverage = read.report
|
|
515
|
+
? propertyCoverage(read.report, expected, gate.report && manifest
|
|
516
|
+
? { cases: manifest.cases, truncated: manifest.truncated }
|
|
517
|
+
: undefined)
|
|
518
|
+
: [];
|
|
519
|
+
if (failure) {
|
|
520
|
+
passed = false;
|
|
521
|
+
summary = join2(failure);
|
|
522
|
+
}
|
|
523
|
+
else if (coverage.length) {
|
|
524
|
+
passed = false;
|
|
525
|
+
summary = join2('Генеративная проверка не подтвердила покрытие: ' + coverage.join('; '));
|
|
526
|
+
}
|
|
527
|
+
else if (passed && read.problem) {
|
|
528
|
+
// Прошедшая команда без отчёта не доказывает свойство: зелёный код
|
|
529
|
+
// выхода был у неё и до реализации.
|
|
530
|
+
passed = false;
|
|
531
|
+
summary = `Генеративная проверка не отчиталась: ${read.problem}`;
|
|
532
|
+
}
|
|
533
|
+
propertyEvidence = {
|
|
534
|
+
seed,
|
|
535
|
+
reproduce: redact(`DEVCONTOUR_SEED=${seed} ${gate.command.join(' ')}`),
|
|
536
|
+
...(read.report
|
|
537
|
+
? { report: redactDeep(read.report, redact) }
|
|
538
|
+
: { problem: redact(read.problem) }),
|
|
539
|
+
...(cleanup && read.report ? { problem: cleanup } : {}),
|
|
540
|
+
contracts: property.contracts,
|
|
541
|
+
artifacts: property.artifacts,
|
|
542
|
+
level: expected ? 'manifest' : 'legacy',
|
|
543
|
+
...(expected
|
|
544
|
+
? {
|
|
545
|
+
manifest: {
|
|
546
|
+
digest: manifestDigest(expected),
|
|
547
|
+
properties: expected.properties.map((p) => p.id),
|
|
548
|
+
...(expected.corpus
|
|
549
|
+
? {
|
|
550
|
+
corpus: { digest: expected.corpus.digest, cases: expected.corpus.cases.length },
|
|
551
|
+
}
|
|
552
|
+
: {}),
|
|
553
|
+
},
|
|
554
|
+
}
|
|
555
|
+
: {}),
|
|
556
|
+
...(coverage.length ? { coverage } : {}),
|
|
557
|
+
};
|
|
558
|
+
if (!passed)
|
|
559
|
+
log += '\n' + summary;
|
|
560
|
+
}
|
|
561
|
+
else if (corpusDir)
|
|
562
|
+
await rm(corpusDir, { recursive: true, force: true }).catch(() => undefined);
|
|
563
|
+
await mkdir(artifactDir, { recursive: true });
|
|
564
|
+
const logPath = join(artifactDir, `${gate.id}.log`);
|
|
565
|
+
log = redact(log);
|
|
566
|
+
await writeFile(logPath, log);
|
|
567
|
+
const evidence = run.record({
|
|
568
|
+
kind: 'test',
|
|
569
|
+
phase,
|
|
570
|
+
sha,
|
|
571
|
+
gate: gate.id,
|
|
572
|
+
passed,
|
|
573
|
+
command: gate.command.map(redact),
|
|
574
|
+
exitCode,
|
|
575
|
+
log: logPath,
|
|
576
|
+
digest: sha256(log),
|
|
577
|
+
summary: redact(summary),
|
|
578
|
+
...(manifest ? { tests: manifest.cases } : {}),
|
|
579
|
+
...(manifest?.truncated ? { testsTruncated: true } : {}),
|
|
580
|
+
...(run.consumer ? { consumer: run.consumer } : {}),
|
|
581
|
+
...(propertyEvidence ? { property: propertyEvidence } : {}),
|
|
582
|
+
});
|
|
583
|
+
if (!passed) {
|
|
584
|
+
// Отказ связывается со свойствами по отчёту проверки, а не по тексту:
|
|
585
|
+
// объявленные поручением — для эпизода восстановления, прочие — для разбора.
|
|
586
|
+
const report = propertyEvidence?.report;
|
|
587
|
+
const failing = [
|
|
588
|
+
...(report?.properties.filter((p) => p.status === 'failed').map((p) => p.testId) ?? []),
|
|
589
|
+
...(report?.corpus?.failed.map((c) => c.property) ?? []),
|
|
590
|
+
];
|
|
591
|
+
const declared = new Set(expected?.properties.map((p) => p.id) ?? []);
|
|
592
|
+
throw new TaskFailure(kind, `${phase}/${run.consumer ? run.consumer + ':' : ''}${gate.id}: ${evidence.summary}`, failing.length
|
|
593
|
+
? {
|
|
594
|
+
bound: [...new Set(failing.filter((id) => declared.has(id)))],
|
|
595
|
+
unbound: [...new Set(failing.filter((id) => !declared.has(id)))],
|
|
596
|
+
}
|
|
597
|
+
: undefined);
|
|
598
|
+
}
|
|
599
|
+
return evidence;
|
|
600
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type ExecutionPolicy, type ExecutionView, type JointVerification, type JointVerificationState } from '../contracts/index.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Совместная проверка закреплённой комбинации SHA репозиториев.
|
|
4
|
+
*
|
|
5
|
+
* Поручения принимаются по одному, каждое — в своём репозитории. Совместная
|
|
6
|
+
* проверка отвечает на другой вопрос: работает ли вместе комбинация их
|
|
7
|
+
* результатов. Она привязана к manifest (SHA и tree каждого репозитория), к
|
|
8
|
+
* политике совместной проверки и к основанию — постановкам, статусам и
|
|
9
|
+
* результатам поручений. Сменилось любое из них — прежний результат
|
|
10
|
+
* устарел. Атомарного продвижения нескольких Git-репозиториев здесь нет;
|
|
11
|
+
* продуктовое принятие комбинации решает хост.
|
|
12
|
+
*/
|
|
13
|
+
/** Политика совместной проверки: всё, от чего зависит её результат. */
|
|
14
|
+
export declare function jointPolicyDigest(policy: ExecutionPolicy): string;
|
|
15
|
+
/** Основание проверки: поручения, их постановки, статусы и результаты. */
|
|
16
|
+
export declare function jointSpecDigest(view: ExecutionView, workIds: string[]): string;
|
|
17
|
+
/** Один и тот же набор поручений — один предмет проверки, независимо от порядка. */
|
|
18
|
+
export declare const sameWorks: (a: string[], b: string[]) => boolean;
|
|
19
|
+
/** Gates совместной проверки в порядке их зависимостей. */
|
|
20
|
+
export declare function orderedWorkspaceGates(gates: ExecutionPolicy['workspaceGates']): ({
|
|
21
|
+
kind: "test";
|
|
22
|
+
report: {
|
|
23
|
+
type: "junit";
|
|
24
|
+
path: string;
|
|
25
|
+
};
|
|
26
|
+
repositoryId: string;
|
|
27
|
+
artifacts: string[];
|
|
28
|
+
id: string;
|
|
29
|
+
command: string[];
|
|
30
|
+
timeoutMs: number;
|
|
31
|
+
property?: {
|
|
32
|
+
path?: string | undefined;
|
|
33
|
+
} | undefined;
|
|
34
|
+
dependsOn?: string[] | undefined;
|
|
35
|
+
cwd?: string | undefined;
|
|
36
|
+
resources?: string[] | undefined;
|
|
37
|
+
} | {
|
|
38
|
+
kind: "check";
|
|
39
|
+
repositoryId: string;
|
|
40
|
+
artifacts: string[];
|
|
41
|
+
id: string;
|
|
42
|
+
command: string[];
|
|
43
|
+
timeoutMs: number;
|
|
44
|
+
report?: {
|
|
45
|
+
type: "junit";
|
|
46
|
+
path: string;
|
|
47
|
+
} | undefined;
|
|
48
|
+
property?: {
|
|
49
|
+
path?: string | undefined;
|
|
50
|
+
} | undefined;
|
|
51
|
+
dependsOn?: string[] | undefined;
|
|
52
|
+
cwd?: string | undefined;
|
|
53
|
+
resources?: string[] | undefined;
|
|
54
|
+
})[];
|
|
55
|
+
/**
|
|
56
|
+
* Какие gates исполнять. По умолчанию — все. В режиме `affected` при
|
|
57
|
+
* известной прежней успешной комбинации под той же политикой — только
|
|
58
|
+
* изменённые компоненты, их транзитивные потребители и prerequisites их
|
|
59
|
+
* проверок; без достаточного теста в этой области — снова все.
|
|
60
|
+
*/
|
|
61
|
+
export declare function jointImpact(policy: ExecutionPolicy, manifest: NonNullable<JointVerification['manifest']>, policyDigest: string, baseline?: JointVerificationState): NonNullable<JointVerification['impact']>;
|
|
62
|
+
/** Публичная форма: без токена и срока владения. */
|
|
63
|
+
export declare function publicJoint(v: JointVerificationState): JointVerification;
|