@xoxoai/checkmate 0.6.7 → 0.7.1
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/README.md +54 -257
- package/dist/checkmate.js +13375 -0
- package/package.json +30 -94
- package/plugin/.claude-plugin/plugin.json +10 -0
- package/plugin/bin/checkmate.ts +3 -0
- package/plugin/contracts/finding.schema.json +78 -0
- package/plugin/contracts/gap.schema.json +56 -0
- package/plugin/contracts/plan-candidate.schema.json +103 -0
- package/plugin/contracts/plan-delta.schema.json +63 -0
- package/plugin/contracts/plan-patch.schema.json +116 -0
- package/plugin/contracts/plan-request.schema.json +108 -0
- package/plugin/contracts/plan.schema.json +313 -0
- package/plugin/contracts/profile.schema.json +154 -0
- package/plugin/contracts/response.schema.json +25 -0
- package/plugin/contracts/result.schema.json +410 -0
- package/plugin/dist/checkmate.js +13012 -0
- package/plugin/package.json +4 -0
- package/plugin/references/browser.md +35 -0
- package/plugin/references/http.md +30 -0
- package/plugin/references/workflow.md +122 -0
- package/plugin/skills/execute/SKILL.md +92 -0
- package/plugin/skills/plan/SKILL.md +94 -0
- package/plugin/skills/report/SKILL.md +28 -0
- package/plugin/skills/revise/SKILL.md +43 -0
- package/plugin/skills/setup/SKILL.md +82 -0
- package/plugin/skills/status/SKILL.md +20 -0
- package/plugin/skills/ui/SKILL.md +26 -0
- package/plugin/src/actor.ts +21 -0
- package/plugin/src/cli.ts +1133 -0
- package/plugin/src/contracts.ts +342 -0
- package/plugin/src/index.ts +52 -0
- package/plugin/src/plans.ts +1028 -0
- package/plugin/src/render.ts +544 -0
- package/plugin/src/runs.ts +868 -0
- package/plugin/src/store.ts +384 -0
- package/plugin/src/targets.ts +205 -0
- package/plugin/src/types.ts +319 -0
- package/plugin/src/ui/assets/client.js +501 -0
- package/plugin/src/ui/assets/favicon.svg +1 -0
- package/plugin/src/ui/assets/fonts/inter-OFL.txt +93 -0
- package/plugin/src/ui/assets/fonts/inter.woff2 +0 -0
- package/plugin/src/ui/assets/fonts/jetbrains-mono-OFL.txt +93 -0
- package/plugin/src/ui/assets/fonts/jetbrains-mono.woff2 +0 -0
- package/plugin/src/ui/assets/fonts/space-grotesk-OFL.txt +93 -0
- package/plugin/src/ui/assets/fonts/space-grotesk.woff2 +0 -0
- package/plugin/src/ui/assets/style.css +1926 -0
- package/plugin/src/ui/edit.ts +305 -0
- package/plugin/src/ui/forms.ts +209 -0
- package/plugin/src/ui/html.ts +130 -0
- package/plugin/src/ui/layout.ts +198 -0
- package/plugin/src/ui/lists.ts +645 -0
- package/plugin/src/ui/plan.ts +577 -0
- package/plugin/src/ui/run.ts +489 -0
- package/plugin/src/ui/server.ts +684 -0
- package/plugin/src/validators.d.ts +13 -0
- package/plugin/src/validators.js +1 -0
- package/plugin/src/views.ts +839 -0
- package/bin/checkmate.js +0 -13
- package/dist/ai/client.d.ts +0 -64
- package/dist/ai/client.d.ts.map +0 -1
- package/dist/ai/client.js +0 -313
- package/dist/ai/client.js.map +0 -1
- package/dist/ai/message-handler.d.ts +0 -9
- package/dist/ai/message-handler.d.ts.map +0 -1
- package/dist/ai/message-handler.js +0 -50
- package/dist/ai/message-handler.js.map +0 -1
- package/dist/ai/message-history.d.ts +0 -17
- package/dist/ai/message-history.d.ts.map +0 -1
- package/dist/ai/message-history.js +0 -45
- package/dist/ai/message-history.js.map +0 -1
- package/dist/ai/prompts.d.ts +0 -4
- package/dist/ai/prompts.d.ts.map +0 -1
- package/dist/ai/prompts.js +0 -24
- package/dist/ai/prompts.js.map +0 -1
- package/dist/ai/rate-limit-policy.d.ts +0 -9
- package/dist/ai/rate-limit-policy.d.ts.map +0 -1
- package/dist/ai/rate-limit-policy.js +0 -34
- package/dist/ai/rate-limit-policy.js.map +0 -1
- package/dist/ai/tool-response-handler.d.ts +0 -16
- package/dist/ai/tool-response-handler.d.ts.map +0 -1
- package/dist/ai/tool-response-handler.js +0 -85
- package/dist/ai/tool-response-handler.js.map +0 -1
- package/dist/ai/turn-processor.d.ts +0 -34
- package/dist/ai/turn-processor.d.ts.map +0 -1
- package/dist/ai/turn-processor.js +0 -107
- package/dist/ai/turn-processor.js.map +0 -1
- package/dist/api/allocate-run-identity.d.ts +0 -8
- package/dist/api/allocate-run-identity.d.ts.map +0 -1
- package/dist/api/allocate-run-identity.js +0 -10
- package/dist/api/allocate-run-identity.js.map +0 -1
- package/dist/api/execute-prepared-run.d.ts +0 -22
- package/dist/api/execute-prepared-run.d.ts.map +0 -1
- package/dist/api/execute-prepared-run.js +0 -184
- package/dist/api/execute-prepared-run.js.map +0 -1
- package/dist/api/index.d.ts +0 -25
- package/dist/api/index.d.ts.map +0 -1
- package/dist/api/index.js +0 -69
- package/dist/api/index.js.map +0 -1
- package/dist/api/prepare-run.d.ts +0 -44
- package/dist/api/prepare-run.d.ts.map +0 -1
- package/dist/api/prepare-run.js +0 -261
- package/dist/api/prepare-run.js.map +0 -1
- package/dist/cli/diagnostic-relay.d.ts +0 -6
- package/dist/cli/diagnostic-relay.d.ts.map +0 -1
- package/dist/cli/diagnostic-relay.js +0 -6
- package/dist/cli/diagnostic-relay.js.map +0 -1
- package/dist/cli/main.d.ts +0 -21
- package/dist/cli/main.d.ts.map +0 -1
- package/dist/cli/main.js +0 -166
- package/dist/cli/main.js.map +0 -1
- package/dist/cli/protocol.d.ts +0 -57
- package/dist/cli/protocol.d.ts.map +0 -1
- package/dist/cli/protocol.js +0 -116
- package/dist/cli/protocol.js.map +0 -1
- package/dist/cli/reconcile-result.d.ts +0 -13
- package/dist/cli/reconcile-result.d.ts.map +0 -1
- package/dist/cli/reconcile-result.js +0 -300
- package/dist/cli/reconcile-result.js.map +0 -1
- package/dist/cli/run-parent.d.ts +0 -37
- package/dist/cli/run-parent.d.ts.map +0 -1
- package/dist/cli/run-parent.js +0 -491
- package/dist/cli/run-parent.js.map +0 -1
- package/dist/cli/signals.d.ts +0 -36
- package/dist/cli/signals.d.ts.map +0 -1
- package/dist/cli/signals.js +0 -85
- package/dist/cli/signals.js.map +0 -1
- package/dist/cli/static-commands.d.ts +0 -11
- package/dist/cli/static-commands.d.ts.map +0 -1
- package/dist/cli/static-commands.js +0 -83
- package/dist/cli/static-commands.js.map +0 -1
- package/dist/cli/worker-stream-guard.d.ts +0 -20
- package/dist/cli/worker-stream-guard.d.ts.map +0 -1
- package/dist/cli/worker-stream-guard.js +0 -41
- package/dist/cli/worker-stream-guard.js.map +0 -1
- package/dist/cli/worker.d.ts +0 -3
- package/dist/cli/worker.d.ts.map +0 -1
- package/dist/cli/worker.js +0 -111
- package/dist/cli/worker.js.map +0 -1
- package/dist/config/ingestion.d.ts +0 -58
- package/dist/config/ingestion.d.ts.map +0 -1
- package/dist/config/ingestion.js +0 -283
- package/dist/config/ingestion.js.map +0 -1
- package/dist/config/manifest.d.ts +0 -20
- package/dist/config/manifest.d.ts.map +0 -1
- package/dist/config/manifest.js +0 -91
- package/dist/config/manifest.js.map +0 -1
- package/dist/config/model-egress.d.ts +0 -10
- package/dist/config/model-egress.d.ts.map +0 -1
- package/dist/config/model-egress.js +0 -62
- package/dist/config/model-egress.js.map +0 -1
- package/dist/config/package-resolution.d.ts +0 -4
- package/dist/config/package-resolution.d.ts.map +0 -1
- package/dist/config/package-resolution.js +0 -24
- package/dist/config/package-resolution.js.map +0 -1
- package/dist/config/policy.d.ts +0 -44
- package/dist/config/policy.d.ts.map +0 -1
- package/dist/config/policy.js +0 -141
- package/dist/config/policy.js.map +0 -1
- package/dist/config/record.d.ts +0 -4
- package/dist/config/record.d.ts.map +0 -1
- package/dist/config/record.js +0 -10
- package/dist/config/record.js.map +0 -1
- package/dist/config/secrets.d.ts +0 -11
- package/dist/config/secrets.d.ts.map +0 -1
- package/dist/config/secrets.js +0 -42
- package/dist/config/secrets.js.map +0 -1
- package/dist/contracts/diagnostics.d.ts +0 -4
- package/dist/contracts/diagnostics.d.ts.map +0 -1
- package/dist/contracts/diagnostics.js +0 -31
- package/dist/contracts/diagnostics.js.map +0 -1
- package/dist/contracts/serialize.d.ts +0 -2
- package/dist/contracts/serialize.d.ts.map +0 -1
- package/dist/contracts/serialize.js +0 -90
- package/dist/contracts/serialize.js.map +0 -1
- package/dist/contracts/types.d.ts +0 -316
- package/dist/contracts/types.d.ts.map +0 -1
- package/dist/contracts/types.js +0 -74
- package/dist/contracts/types.js.map +0 -1
- package/dist/contracts/validator.d.ts +0 -8
- package/dist/contracts/validator.d.ts.map +0 -1
- package/dist/contracts/validator.js +0 -71
- package/dist/contracts/validator.js.map +0 -1
- package/dist/driver.d.ts +0 -97
- package/dist/driver.d.ts.map +0 -1
- package/dist/driver.js +0 -24
- package/dist/driver.js.map +0 -1
- package/dist/drivers/descriptor.d.ts +0 -15
- package/dist/drivers/descriptor.d.ts.map +0 -1
- package/dist/drivers/descriptor.js +0 -122
- package/dist/drivers/descriptor.js.map +0 -1
- package/dist/drivers/loader.d.ts +0 -18
- package/dist/drivers/loader.d.ts.map +0 -1
- package/dist/drivers/loader.js +0 -97
- package/dist/drivers/loader.js.map +0 -1
- package/dist/drivers/web/checkmate-driver.json +0 -47
- package/dist/drivers/web/index.d.ts +0 -6
- package/dist/drivers/web/index.d.ts.map +0 -1
- package/dist/drivers/web/index.js +0 -27
- package/dist/drivers/web/index.js.map +0 -1
- package/dist/drivers/web/session.d.ts +0 -25
- package/dist/drivers/web/session.d.ts.map +0 -1
- package/dist/drivers/web/session.js +0 -132
- package/dist/drivers/web/session.js.map +0 -1
- package/dist/drivers/web/tools/network-request-recorder.d.ts +0 -46
- package/dist/drivers/web/tools/network-request-recorder.d.ts.map +0 -1
- package/dist/drivers/web/tools/network-request-recorder.js +0 -164
- package/dist/drivers/web/tools/network-request-recorder.js.map +0 -1
- package/dist/drivers/web/tools/screenshot-service.d.ts +0 -10
- package/dist/drivers/web/tools/screenshot-service.d.ts.map +0 -1
- package/dist/drivers/web/tools/screenshot-service.js +0 -23
- package/dist/drivers/web/tools/screenshot-service.js.map +0 -1
- package/dist/drivers/web/tools/snapshot-filter/index.d.ts +0 -4
- package/dist/drivers/web/tools/snapshot-filter/index.d.ts.map +0 -1
- package/dist/drivers/web/tools/snapshot-filter/index.js +0 -4
- package/dist/drivers/web/tools/snapshot-filter/index.js.map +0 -1
- package/dist/drivers/web/tools/snapshot-filter/semantic-scorer.d.ts +0 -15
- package/dist/drivers/web/tools/snapshot-filter/semantic-scorer.d.ts.map +0 -1
- package/dist/drivers/web/tools/snapshot-filter/semantic-scorer.js +0 -99
- package/dist/drivers/web/tools/snapshot-filter/semantic-scorer.js.map +0 -1
- package/dist/drivers/web/tools/snapshot-filter/snapshot-filter.d.ts +0 -5
- package/dist/drivers/web/tools/snapshot-filter/snapshot-filter.d.ts.map +0 -1
- package/dist/drivers/web/tools/snapshot-filter/snapshot-filter.js +0 -56
- package/dist/drivers/web/tools/snapshot-filter/snapshot-filter.js.map +0 -1
- package/dist/drivers/web/tools/snapshot-filter/tree-reconstructor.d.ts +0 -3
- package/dist/drivers/web/tools/snapshot-filter/tree-reconstructor.d.ts.map +0 -1
- package/dist/drivers/web/tools/snapshot-filter/tree-reconstructor.js +0 -85
- package/dist/drivers/web/tools/snapshot-filter/tree-reconstructor.js.map +0 -1
- package/dist/drivers/web/tools/snapshot-service.d.ts +0 -31
- package/dist/drivers/web/tools/snapshot-service.d.ts.map +0 -1
- package/dist/drivers/web/tools/snapshot-service.js +0 -96
- package/dist/drivers/web/tools/snapshot-service.js.map +0 -1
- package/dist/drivers/web/tools/tool.d.ts +0 -76
- package/dist/drivers/web/tools/tool.d.ts.map +0 -1
- package/dist/drivers/web/tools/tool.js +0 -562
- package/dist/drivers/web/tools/tool.js.map +0 -1
- package/dist/drivers/web/tools/transient-state-tracker.d.ts +0 -26
- package/dist/drivers/web/tools/transient-state-tracker.d.ts.map +0 -1
- package/dist/drivers/web/tools/transient-state-tracker.js +0 -301
- package/dist/drivers/web/tools/transient-state-tracker.js.map +0 -1
- package/dist/drivers/web/tools/types.d.ts +0 -11
- package/dist/drivers/web/tools/types.d.ts.map +0 -1
- package/dist/drivers/web/tools/types.js +0 -2
- package/dist/drivers/web/tools/types.js.map +0 -1
- package/dist/evidence/atomic-file.d.ts +0 -23
- package/dist/evidence/atomic-file.d.ts.map +0 -1
- package/dist/evidence/atomic-file.js +0 -104
- package/dist/evidence/atomic-file.js.map +0 -1
- package/dist/evidence/layout.d.ts +0 -30
- package/dist/evidence/layout.d.ts.map +0 -1
- package/dist/evidence/layout.js +0 -152
- package/dist/evidence/layout.js.map +0 -1
- package/dist/evidence/retention.d.ts +0 -5
- package/dist/evidence/retention.d.ts.map +0 -1
- package/dist/evidence/retention.js +0 -10
- package/dist/evidence/retention.js.map +0 -1
- package/dist/evidence/store.d.ts +0 -101
- package/dist/evidence/store.d.ts.map +0 -1
- package/dist/evidence/store.js +0 -375
- package/dist/evidence/store.js.map +0 -1
- package/dist/evidence/terminal-finalizer.d.ts +0 -19
- package/dist/evidence/terminal-finalizer.d.ts.map +0 -1
- package/dist/evidence/terminal-finalizer.js +0 -56
- package/dist/evidence/terminal-finalizer.js.map +0 -1
- package/dist/index.d.ts +0 -9
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -12
- package/dist/index.js.map +0 -1
- package/dist/logging/invocation-logger.d.ts +0 -4
- package/dist/logging/invocation-logger.d.ts.map +0 -1
- package/dist/logging/invocation-logger.js +0 -25
- package/dist/logging/invocation-logger.js.map +0 -1
- package/dist/logging/level-logger.d.ts +0 -11
- package/dist/logging/level-logger.d.ts.map +0 -1
- package/dist/logging/level-logger.js +0 -82
- package/dist/logging/level-logger.js.map +0 -1
- package/dist/logging/types.d.ts +0 -8
- package/dist/logging/types.d.ts.map +0 -1
- package/dist/logging/types.js +0 -7
- package/dist/logging/types.js.map +0 -1
- package/dist/redaction/diagnostic-sanitizer.d.ts +0 -7
- package/dist/redaction/diagnostic-sanitizer.d.ts.map +0 -1
- package/dist/redaction/diagnostic-sanitizer.js +0 -42
- package/dist/redaction/diagnostic-sanitizer.js.map +0 -1
- package/dist/redaction/redactor.d.ts +0 -40
- package/dist/redaction/redactor.d.ts.map +0 -1
- package/dist/redaction/redactor.js +0 -176
- package/dist/redaction/redactor.js.map +0 -1
- package/dist/redaction/scrub.d.ts +0 -29
- package/dist/redaction/scrub.d.ts.map +0 -1
- package/dist/redaction/scrub.js +0 -54
- package/dist/redaction/scrub.js.map +0 -1
- package/dist/runtime/config.d.ts +0 -16
- package/dist/runtime/config.d.ts.map +0 -1
- package/dist/runtime/config.js +0 -2
- package/dist/runtime/config.js.map +0 -1
- package/dist/runtime/driver-boundary.d.ts +0 -16
- package/dist/runtime/driver-boundary.d.ts.map +0 -1
- package/dist/runtime/driver-boundary.js +0 -71
- package/dist/runtime/driver-boundary.js.map +0 -1
- package/dist/runtime/driver-session.d.ts +0 -9
- package/dist/runtime/driver-session.d.ts.map +0 -1
- package/dist/runtime/driver-session.js +0 -64
- package/dist/runtime/driver-session.js.map +0 -1
- package/dist/runtime/internal-step-evidence.d.ts +0 -30
- package/dist/runtime/internal-step-evidence.d.ts.map +0 -1
- package/dist/runtime/internal-step-evidence.js +0 -82
- package/dist/runtime/internal-step-evidence.js.map +0 -1
- package/dist/runtime/result-adapter.d.ts +0 -9
- package/dist/runtime/result-adapter.d.ts.map +0 -1
- package/dist/runtime/result-adapter.js +0 -77
- package/dist/runtime/result-adapter.js.map +0 -1
- package/dist/runtime/runner.d.ts +0 -43
- package/dist/runtime/runner.d.ts.map +0 -1
- package/dist/runtime/runner.js +0 -72
- package/dist/runtime/runner.js.map +0 -1
- package/dist/runtime/scenario-control.d.ts +0 -40
- package/dist/runtime/scenario-control.d.ts.map +0 -1
- package/dist/runtime/scenario-control.js +0 -94
- package/dist/runtime/scenario-control.js.map +0 -1
- package/dist/runtime/scenario-runner.d.ts +0 -41
- package/dist/runtime/scenario-runner.d.ts.map +0 -1
- package/dist/runtime/scenario-runner.js +0 -274
- package/dist/runtime/scenario-runner.js.map +0 -1
- package/dist/runtime/scenario-state.d.ts +0 -55
- package/dist/runtime/scenario-state.d.ts.map +0 -1
- package/dist/runtime/scenario-state.js +0 -128
- package/dist/runtime/scenario-state.js.map +0 -1
- package/dist/runtime/step-execution.d.ts +0 -27
- package/dist/runtime/step-execution.d.ts.map +0 -1
- package/dist/runtime/step-execution.js +0 -143
- package/dist/runtime/step-execution.js.map +0 -1
- package/dist/runtime/text.d.ts +0 -10
- package/dist/runtime/text.d.ts.map +0 -1
- package/dist/runtime/text.js +0 -25
- package/dist/runtime/text.js.map +0 -1
- package/dist/runtime/types.d.ts +0 -53
- package/dist/runtime/types.d.ts.map +0 -1
- package/dist/runtime/types.js +0 -2
- package/dist/runtime/types.js.map +0 -1
- package/dist/runtime/usage-tracker.d.ts +0 -37
- package/dist/runtime/usage-tracker.d.ts.map +0 -1
- package/dist/runtime/usage-tracker.js +0 -110
- package/dist/runtime/usage-tracker.js.map +0 -1
- package/dist/tools/define-agent-tool.d.ts +0 -20
- package/dist/tools/define-agent-tool.d.ts.map +0 -1
- package/dist/tools/define-agent-tool.js +0 -30
- package/dist/tools/define-agent-tool.js.map +0 -1
- package/dist/tools/dispatcher.d.ts +0 -19
- package/dist/tools/dispatcher.d.ts.map +0 -1
- package/dist/tools/dispatcher.js +0 -91
- package/dist/tools/dispatcher.js.map +0 -1
- package/dist/tools/loop-detector.d.ts +0 -27
- package/dist/tools/loop-detector.d.ts.map +0 -1
- package/dist/tools/loop-detector.js +0 -76
- package/dist/tools/loop-detector.js.map +0 -1
- package/dist/tools/registry.d.ts +0 -19
- package/dist/tools/registry.d.ts.map +0 -1
- package/dist/tools/registry.js +0 -54
- package/dist/tools/registry.js.map +0 -1
- package/dist/tools/step/result-tool.d.ts +0 -7
- package/dist/tools/step/result-tool.d.ts.map +0 -1
- package/dist/tools/step/result-tool.js +0 -34
- package/dist/tools/step/result-tool.js.map +0 -1
- package/dist/tools/tool-contract.d.ts +0 -3
- package/dist/tools/tool-contract.d.ts.map +0 -1
- package/dist/tools/tool-contract.js +0 -2
- package/dist/tools/tool-contract.js.map +0 -1
- package/dist/tools/types.d.ts +0 -210
- package/dist/tools/types.d.ts.map +0 -1
- package/dist/tools/types.js +0 -4
- package/dist/tools/types.js.map +0 -1
- package/docs/CLI.md +0 -59
- package/docs/CONFIGURATION.md +0 -105
- package/docs/DRIVERS.md +0 -79
- package/docs/EVIDENCE.md +0 -64
- package/schemas/checkmate-config.v1.json +0 -176
- package/schemas/describe-result.v1.json +0 -414
- package/schemas/driver-descriptor.v1.json +0 -66
- package/schemas/run-request.v1.json +0 -67
- package/schemas/run-result.v1.json +0 -333
- package/schemas/validation-result.v1.json +0 -86
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
import type { Gap, Plan, Result } from './types.ts'
|
|
2
|
+
import type { Store } from './store.ts'
|
|
3
|
+
import { all, canonical, completed, digest, execute, one, removeTemporaryDirectory, ulid, write } from './store.ts'
|
|
4
|
+
import type { AssertionResult, ResultTest, Test } from './contracts.ts'
|
|
5
|
+
import {
|
|
6
|
+
approvalPolicy,
|
|
7
|
+
expectDigest,
|
|
8
|
+
requireChoice,
|
|
9
|
+
requireCondition,
|
|
10
|
+
requireIdentifier,
|
|
11
|
+
requireText,
|
|
12
|
+
resultCoverage,
|
|
13
|
+
testOutcome,
|
|
14
|
+
testReference,
|
|
15
|
+
validate,
|
|
16
|
+
} from './contracts.ts'
|
|
17
|
+
import { approvalState, dependencyBlock, gate, latestPlans, recordApproval } from './plans.ts'
|
|
18
|
+
import { targetRevision } from './targets.ts'
|
|
19
|
+
import { mkdirSync, readFileSync, realpathSync, rmSync, statSync } from 'node:fs'
|
|
20
|
+
import { basename, extname, isAbsolute, join, resolve, sep } from 'node:path'
|
|
21
|
+
import { randomUUID } from 'node:crypto'
|
|
22
|
+
|
|
23
|
+
export type RunRow = {
|
|
24
|
+
run_id: string
|
|
25
|
+
plan_id: string
|
|
26
|
+
plan_revision: number
|
|
27
|
+
plan_sha256: string
|
|
28
|
+
target_id: string
|
|
29
|
+
target_revision: number
|
|
30
|
+
application_build: string
|
|
31
|
+
request: string
|
|
32
|
+
started_at: string
|
|
33
|
+
browser_session: string
|
|
34
|
+
deadline_seconds: number
|
|
35
|
+
}
|
|
36
|
+
export type RunRequest = {
|
|
37
|
+
selectedTestIds: string[]
|
|
38
|
+
partialExecution: string
|
|
39
|
+
onNewBlocker: string
|
|
40
|
+
timeoutSeconds: number
|
|
41
|
+
retestOf: string | null
|
|
42
|
+
}
|
|
43
|
+
export type RunState = { row: RunRow; request: RunRequest; plan: Plan; finished: boolean }
|
|
44
|
+
export type OutcomeRow = { test_id: string; attempt: number; outcome: ResultTest['outcome']; reason: string | null }
|
|
45
|
+
export type AssertionRow = {
|
|
46
|
+
test_id: string
|
|
47
|
+
assertion_id: string
|
|
48
|
+
attempt: number
|
|
49
|
+
result: AssertionResult
|
|
50
|
+
observed: string | null
|
|
51
|
+
reason: string | null
|
|
52
|
+
}
|
|
53
|
+
export type EvidenceRow = {
|
|
54
|
+
id: string
|
|
55
|
+
test_id: string
|
|
56
|
+
assertion_id: string | null
|
|
57
|
+
kind: Result['evidence'][number]['kind']
|
|
58
|
+
name: string
|
|
59
|
+
media_type: string
|
|
60
|
+
sha256: string
|
|
61
|
+
size: number
|
|
62
|
+
}
|
|
63
|
+
export type BeginInput = {
|
|
64
|
+
runId: string
|
|
65
|
+
reference: string
|
|
66
|
+
tests?: string[]
|
|
67
|
+
build: string
|
|
68
|
+
partial?: string
|
|
69
|
+
onNewBlocker?: string
|
|
70
|
+
timeoutSeconds?: number
|
|
71
|
+
retestOf?: string | null
|
|
72
|
+
}
|
|
73
|
+
export type EvidenceInput = string | { text: string; kind?: string }
|
|
74
|
+
export type CheckInput = {
|
|
75
|
+
runId: string
|
|
76
|
+
testId: string
|
|
77
|
+
assertionId: string
|
|
78
|
+
result: string
|
|
79
|
+
observed?: string
|
|
80
|
+
reason?: string
|
|
81
|
+
evidence?: EvidenceInput[]
|
|
82
|
+
}
|
|
83
|
+
export type CloseInput = {
|
|
84
|
+
runId: string
|
|
85
|
+
testId: string
|
|
86
|
+
outcome?: string
|
|
87
|
+
reason?: string
|
|
88
|
+
evidence?: EvidenceInput[]
|
|
89
|
+
}
|
|
90
|
+
export type FinishInput = { runId: string; completion: string; cleanup: string; cleanupReason: string }
|
|
91
|
+
export type ResumeInput = { runId: string; error?: string[]; reason?: string }
|
|
92
|
+
export type ResultVerdict = { runId: string; rationale: string; expectSha256?: string }
|
|
93
|
+
|
|
94
|
+
export const maxEvidenceBytes = 32 * 1024 * 1024
|
|
95
|
+
export const stoppedReason = 'Run stopped before the test closed. Actions may have occurred.'
|
|
96
|
+
const kindByExtension: Record<string, EvidenceRow['kind']> = {
|
|
97
|
+
'.png': 'screenshot',
|
|
98
|
+
'.jpg': 'screenshot',
|
|
99
|
+
'.jpeg': 'screenshot',
|
|
100
|
+
'.webp': 'screenshot',
|
|
101
|
+
'.json': 'http_exchange',
|
|
102
|
+
'.zip': 'trace',
|
|
103
|
+
'.trace': 'trace',
|
|
104
|
+
}
|
|
105
|
+
const mediaTypes: Record<string, string> = {
|
|
106
|
+
'.png': 'image/png',
|
|
107
|
+
'.jpg': 'image/jpeg',
|
|
108
|
+
'.jpeg': 'image/jpeg',
|
|
109
|
+
'.webp': 'image/webp',
|
|
110
|
+
'.gif': 'image/gif',
|
|
111
|
+
'.json': 'application/json',
|
|
112
|
+
'.txt': 'text/plain',
|
|
113
|
+
'.log': 'text/plain',
|
|
114
|
+
'.md': 'text/markdown',
|
|
115
|
+
'.html': 'text/html',
|
|
116
|
+
'.zip': 'application/zip',
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function evidenceDirectory(runId: string) {
|
|
120
|
+
return `.checkmate/tmp/${runId}/`
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function runState(store: Store, runId: string): RunState {
|
|
124
|
+
const row = one<RunRow>(store, 'SELECT * FROM runs WHERE run_id = ?', runId)
|
|
125
|
+
requireCondition(row, `Unknown run: ${runId}`)
|
|
126
|
+
const plan = one<{ body: string }>(
|
|
127
|
+
store,
|
|
128
|
+
'SELECT body FROM plan_revisions WHERE plan_id = ? AND revision = ?',
|
|
129
|
+
row.plan_id,
|
|
130
|
+
row.plan_revision
|
|
131
|
+
)!
|
|
132
|
+
return {
|
|
133
|
+
row,
|
|
134
|
+
request: JSON.parse(row.request),
|
|
135
|
+
plan: JSON.parse(plan.body),
|
|
136
|
+
finished: Boolean(one(store, 'SELECT 1 FROM results WHERE run_id = ?', runId)),
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function startedRun(store: Store, runId: string) {
|
|
141
|
+
const run = runState(store, runId)
|
|
142
|
+
requireCondition(!run.finished, `Run ${runId} is finished. A finished run does not change.`)
|
|
143
|
+
return run
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function selectedTest(run: RunState, testId: string): Test {
|
|
147
|
+
requireCondition(
|
|
148
|
+
run.request.selectedTestIds.includes(testId),
|
|
149
|
+
`Test ${testId} is not selected in run ${run.row.run_id}`
|
|
150
|
+
)
|
|
151
|
+
return run.plan.tests.find((test) => test.id === testId)!
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function currentAttempt(store: Store, runId: string) {
|
|
155
|
+
return one<{ attempt: number }>(store, 'SELECT max(attempt) AS attempt FROM run_attempts WHERE run_id = ?', runId)!
|
|
156
|
+
.attempt
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function runOutcomes(store: Store, runId: string) {
|
|
160
|
+
const rows = all<OutcomeRow>(store, 'SELECT * FROM outcomes WHERE run_id = ? ORDER BY rowid', runId)
|
|
161
|
+
return new Map(rows.map((row) => [row.test_id, row]))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function runAssertions(store: Store, runId: string) {
|
|
165
|
+
const rows = all<AssertionRow>(store, 'SELECT * FROM assertion_results WHERE run_id = ? ORDER BY rowid', runId)
|
|
166
|
+
return new Map(rows.map((row) => [`${row.test_id}.${row.assertion_id}`, row]))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function runEvidence(store: Store, runId: string) {
|
|
170
|
+
return all<EvidenceRow>(
|
|
171
|
+
store,
|
|
172
|
+
'SELECT e.id, e.test_id, e.assertion_id, e.kind, e.name, e.media_type, e.sha256, b.size FROM evidence e JOIN blobs b ON b.sha256 = e.sha256 WHERE e.run_id = ? ORDER BY e.rowid',
|
|
173
|
+
runId
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function runFindings(store: Store, runId: string): Result['findings'] {
|
|
178
|
+
return all<{ body: string }>(store, 'SELECT body FROM findings WHERE run_id = ? ORDER BY rowid', runId).map((row) =>
|
|
179
|
+
JSON.parse(row.body)
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function runGaps(store: Store, runId: string): Gap[] {
|
|
184
|
+
return all<{ body: string }>(store, 'SELECT body FROM run_gaps WHERE run_id = ? ORDER BY rowid', runId).map((row) =>
|
|
185
|
+
JSON.parse(row.body)
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function beginRun(store: Store, actor: string | undefined, input: BeginInput) {
|
|
190
|
+
const reply = write(
|
|
191
|
+
store,
|
|
192
|
+
{ actor, command: 'run begin', subject: input.runId, input },
|
|
193
|
+
(operationId, at, actor) => {
|
|
194
|
+
const runId = requireIdentifier(input.runId, 'run ID')
|
|
195
|
+
requireCondition(
|
|
196
|
+
!one(store, 'SELECT 1 FROM runs WHERE run_id = ?', runId),
|
|
197
|
+
`Run ${runId} exists. Use a new run ID.`
|
|
198
|
+
)
|
|
199
|
+
const onNewBlocker = requireChoice(
|
|
200
|
+
input.onNewBlocker ?? 'return_to_oracle',
|
|
201
|
+
['return_to_oracle', 'continue_independent'],
|
|
202
|
+
'new blocker policy (--on-new-blocker)'
|
|
203
|
+
)
|
|
204
|
+
const timeoutSeconds = input.timeoutSeconds ?? 600
|
|
205
|
+
requireCondition(
|
|
206
|
+
Number.isInteger(timeoutSeconds) && timeoutSeconds > 0 && timeoutSeconds <= 3600,
|
|
207
|
+
`Invalid timeout: ${timeoutSeconds}. Use 1 to 3600 seconds.`
|
|
208
|
+
)
|
|
209
|
+
const checked = gate(store, input)
|
|
210
|
+
if (checked.reply.status !== 'completed')
|
|
211
|
+
return { reply: { ...checked.reply, subject: runId }, subjects: [] }
|
|
212
|
+
const { plan, sha256 } = checked.revision
|
|
213
|
+
const request: RunRequest = {
|
|
214
|
+
selectedTestIds: checked.tests,
|
|
215
|
+
partialExecution: checked.partial,
|
|
216
|
+
onNewBlocker,
|
|
217
|
+
timeoutSeconds,
|
|
218
|
+
retestOf: input.retestOf ?? null,
|
|
219
|
+
}
|
|
220
|
+
const browserSession = `cm-${randomUUID()}`
|
|
221
|
+
execute(
|
|
222
|
+
store,
|
|
223
|
+
'INSERT INTO runs (run_id, plan_id, plan_revision, plan_sha256, target_id, target_revision, application_build, request, started_at, browser_session, deadline_seconds, operation_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
|
224
|
+
runId,
|
|
225
|
+
plan.planId,
|
|
226
|
+
plan.revision,
|
|
227
|
+
sha256,
|
|
228
|
+
plan.target.targetId,
|
|
229
|
+
plan.target.revision,
|
|
230
|
+
checked.build,
|
|
231
|
+
canonical(request),
|
|
232
|
+
at,
|
|
233
|
+
browserSession,
|
|
234
|
+
timeoutSeconds,
|
|
235
|
+
operationId
|
|
236
|
+
)
|
|
237
|
+
execute(
|
|
238
|
+
store,
|
|
239
|
+
'INSERT INTO run_attempts (run_id, attempt, started_at, actor) VALUES (?, 1, ?, ?)',
|
|
240
|
+
runId,
|
|
241
|
+
at,
|
|
242
|
+
actor
|
|
243
|
+
)
|
|
244
|
+
return {
|
|
245
|
+
reply: completed(runId, {
|
|
246
|
+
runId,
|
|
247
|
+
plan: `${plan.planId}@${plan.revision}`,
|
|
248
|
+
tests: checked.tests,
|
|
249
|
+
leftOut: checked.reply.data.leftOut,
|
|
250
|
+
startedAt: at,
|
|
251
|
+
browserSession,
|
|
252
|
+
deadlineSeconds: timeoutSeconds,
|
|
253
|
+
evidenceDirectory: evidenceDirectory(runId),
|
|
254
|
+
}),
|
|
255
|
+
subjects: [`run:${runId}`, `plan:${plan.planId}`],
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
)
|
|
259
|
+
if (reply.status === 'completed')
|
|
260
|
+
mkdirSync(join(store.workspace, evidenceDirectory(input.runId)), { recursive: true })
|
|
261
|
+
return reply
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
type EvidenceFile = { path: string | null; name: string; kind: string; mediaType: string; bytes: Buffer }
|
|
265
|
+
|
|
266
|
+
const evidenceKinds = ['observation', 'http_exchange', 'screenshot', 'trace'] as const
|
|
267
|
+
|
|
268
|
+
function textEvidence(input: { text: string; kind?: string }, name: string): EvidenceFile {
|
|
269
|
+
requireCondition(
|
|
270
|
+
typeof input.text === 'string' && input.text.trim().length > 0,
|
|
271
|
+
'Missing evidence text on standard input'
|
|
272
|
+
)
|
|
273
|
+
const bytes = Buffer.from(input.text, 'utf8')
|
|
274
|
+
requireCondition(bytes.length <= maxEvidenceBytes, 'Evidence text exceeds 32 MiB')
|
|
275
|
+
let json = true
|
|
276
|
+
try {
|
|
277
|
+
JSON.parse(input.text)
|
|
278
|
+
} catch {
|
|
279
|
+
json = false
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
path: null,
|
|
283
|
+
name: `${name}.${json ? 'json' : 'txt'}`,
|
|
284
|
+
kind: requireChoice(input.kind ?? 'observation', evidenceKinds, 'evidence kind'),
|
|
285
|
+
mediaType: json ? 'application/json' : 'text/plain',
|
|
286
|
+
bytes,
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function readEvidence(store: Store, runId: string, spec: string): EvidenceFile {
|
|
291
|
+
const match = /^(.*):(observation|http_exchange|screenshot|trace)$/.exec(spec)
|
|
292
|
+
const name = match ? match[1] : spec
|
|
293
|
+
requireCondition(name && !isAbsolute(name), `Evidence path must be relative to the workspace: ${name}`)
|
|
294
|
+
const directory = evidenceDirectory(runId)
|
|
295
|
+
let root: string
|
|
296
|
+
let real: string
|
|
297
|
+
try {
|
|
298
|
+
root = realpathSync(join(store.workspace, directory))
|
|
299
|
+
} catch {
|
|
300
|
+
throw new Error(`Evidence directory ${directory} does not exist`)
|
|
301
|
+
}
|
|
302
|
+
const path = resolve(store.workspace, name)
|
|
303
|
+
try {
|
|
304
|
+
real = realpathSync(path)
|
|
305
|
+
} catch {
|
|
306
|
+
throw new Error(`Evidence file not found: ${name}`)
|
|
307
|
+
}
|
|
308
|
+
requireCondition(real.startsWith(root + sep), `Evidence file must be inside ${directory}: ${name}`)
|
|
309
|
+
const stats = statSync(real)
|
|
310
|
+
requireCondition(stats.isFile(), `Evidence must be a file: ${name}`)
|
|
311
|
+
requireCondition(stats.size <= maxEvidenceBytes, `Evidence file exceeds 32 MiB: ${name}`)
|
|
312
|
+
const extension = extname(name).toLowerCase()
|
|
313
|
+
return {
|
|
314
|
+
path,
|
|
315
|
+
name: basename(name),
|
|
316
|
+
kind: match ? match[2] : (kindByExtension[extension] ?? 'observation'),
|
|
317
|
+
mediaType: mediaTypes[extension] ?? 'application/octet-stream',
|
|
318
|
+
bytes: readFileSync(real),
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function storeEvidence(
|
|
323
|
+
store: Store,
|
|
324
|
+
runId: string,
|
|
325
|
+
testId: string,
|
|
326
|
+
assertionId: string | null,
|
|
327
|
+
files: EvidenceFile[],
|
|
328
|
+
operationId: string
|
|
329
|
+
) {
|
|
330
|
+
return files.map((file) => {
|
|
331
|
+
const sha256 = digest(file.bytes)
|
|
332
|
+
execute(
|
|
333
|
+
store,
|
|
334
|
+
'INSERT OR IGNORE INTO blobs (sha256, size, bytes) VALUES (?, ?, ?)',
|
|
335
|
+
sha256,
|
|
336
|
+
file.bytes.length,
|
|
337
|
+
file.bytes
|
|
338
|
+
)
|
|
339
|
+
const id = ulid()
|
|
340
|
+
execute(
|
|
341
|
+
store,
|
|
342
|
+
'INSERT INTO evidence (id, run_id, test_id, assertion_id, kind, name, media_type, sha256, operation_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
|
343
|
+
id,
|
|
344
|
+
runId,
|
|
345
|
+
testId,
|
|
346
|
+
assertionId,
|
|
347
|
+
file.kind,
|
|
348
|
+
file.name,
|
|
349
|
+
file.mediaType,
|
|
350
|
+
sha256,
|
|
351
|
+
operationId
|
|
352
|
+
)
|
|
353
|
+
return id
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function evidenceFiles(store: Store, runId: string, name: string, specs: EvidenceInput[] = []) {
|
|
358
|
+
const files = specs.map((spec) =>
|
|
359
|
+
typeof spec === 'string' ? readEvidence(store, runId, spec) : textEvidence(spec, name)
|
|
360
|
+
)
|
|
361
|
+
const paths = files.flatMap((file) => file.path ?? [])
|
|
362
|
+
requireCondition(new Set(paths).size === paths.length, 'An evidence file is listed twice')
|
|
363
|
+
requireCondition(files.length - paths.length <= 1, 'Give one evidence text on standard input')
|
|
364
|
+
return files
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function removeFiles(files: EvidenceFile[]) {
|
|
368
|
+
for (const file of files) if (file.path) rmSync(file.path, { force: true })
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function optionalText(value: string | undefined, name: string) {
|
|
372
|
+
return value === undefined ? null : requireText(value, name)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function requireOpen(store: Store, runId: string, testId: string) {
|
|
376
|
+
const closed = runOutcomes(store, runId).get(testId)
|
|
377
|
+
requireCondition(!closed, `Test ${testId} is closed as ${closed?.outcome}. A test outcome is final.`)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function checkAssertion(store: Store, actor: string | undefined, input: CheckInput) {
|
|
381
|
+
let files: EvidenceFile[] = []
|
|
382
|
+
const reply = write(store, { actor, command: 'run check', subject: input.runId, input }, (operationId, at) => {
|
|
383
|
+
const run = startedRun(store, input.runId)
|
|
384
|
+
const test = selectedTest(run, input.testId)
|
|
385
|
+
const assertion = test.assertions.find((assertion) => assertion.id === input.assertionId)
|
|
386
|
+
requireCondition(assertion, `Unknown assertion ${test.id}.${input.assertionId}`)
|
|
387
|
+
requireOpen(store, input.runId, test.id)
|
|
388
|
+
requireCondition(
|
|
389
|
+
!runAssertions(store, input.runId).has(`${test.id}.${assertion.id}`),
|
|
390
|
+
`Assertion ${test.id}.${assertion.id} has a result. An assertion result is final.`
|
|
391
|
+
)
|
|
392
|
+
const outcomes = runOutcomes(store, input.runId)
|
|
393
|
+
for (const dependency of test.dependsOn.filter((id) => !id.includes(':')))
|
|
394
|
+
requireCondition(
|
|
395
|
+
outcomes.get(dependency)?.outcome === 'passed',
|
|
396
|
+
`Prerequisite ${dependency} has not passed. Close ${dependency} as passed before you check ${test.id}.`
|
|
397
|
+
)
|
|
398
|
+
const result = requireChoice(
|
|
399
|
+
input.result,
|
|
400
|
+
['passed', 'failed', 'inconclusive', 'not_checked'] as const,
|
|
401
|
+
'result (--result)'
|
|
402
|
+
)
|
|
403
|
+
const observed = optionalText(input.observed, 'observation (--observed)')
|
|
404
|
+
const reason = optionalText(input.reason, 'reason (--reason)')
|
|
405
|
+
files = evidenceFiles(store, input.runId, `${test.id}-${assertion.id}`, input.evidence)
|
|
406
|
+
if (result !== 'not_checked')
|
|
407
|
+
requireCondition(
|
|
408
|
+
observed && files.length > 0,
|
|
409
|
+
`${result === 'inconclusive' ? 'An' : 'A'} ${result} result needs an observation (--observed) and one or more evidence items (--evidence)`
|
|
410
|
+
)
|
|
411
|
+
if (result === 'inconclusive')
|
|
412
|
+
requireCondition(
|
|
413
|
+
reason,
|
|
414
|
+
'An inconclusive result needs a reason (--reason) that says what the evidence cannot show'
|
|
415
|
+
)
|
|
416
|
+
if (result === 'not_checked')
|
|
417
|
+
requireCondition(reason, 'A not_checked result needs a reason (--reason) that says why it was not checked')
|
|
418
|
+
execute(
|
|
419
|
+
store,
|
|
420
|
+
'INSERT INTO assertion_results (run_id, test_id, assertion_id, attempt, result, observed, reason, recorded_at, operation_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
|
421
|
+
input.runId,
|
|
422
|
+
test.id,
|
|
423
|
+
assertion.id,
|
|
424
|
+
currentAttempt(store, input.runId),
|
|
425
|
+
result,
|
|
426
|
+
observed,
|
|
427
|
+
reason,
|
|
428
|
+
at,
|
|
429
|
+
operationId
|
|
430
|
+
)
|
|
431
|
+
const evidenceIds = storeEvidence(store, input.runId, test.id, assertion.id, files, operationId)
|
|
432
|
+
return {
|
|
433
|
+
reply: completed(input.runId, {
|
|
434
|
+
runId: input.runId,
|
|
435
|
+
testId: test.id,
|
|
436
|
+
assertionId: assertion.id,
|
|
437
|
+
result,
|
|
438
|
+
evidenceIds,
|
|
439
|
+
}),
|
|
440
|
+
subjects: [`run:${input.runId}`],
|
|
441
|
+
}
|
|
442
|
+
})
|
|
443
|
+
removeFiles(files)
|
|
444
|
+
return reply
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export function closeTest(store: Store, actor: string | undefined, input: CloseInput) {
|
|
448
|
+
let files: EvidenceFile[] = []
|
|
449
|
+
const reply = write(store, { actor, command: 'run close', subject: input.runId, input }, (operationId, at) => {
|
|
450
|
+
const run = startedRun(store, input.runId)
|
|
451
|
+
const test = selectedTest(run, input.testId)
|
|
452
|
+
requireOpen(store, input.runId, test.id)
|
|
453
|
+
const results = runAssertions(store, input.runId)
|
|
454
|
+
const recorded = test.assertions.flatMap((assertion) => results.get(`${test.id}.${assertion.id}`) ?? [])
|
|
455
|
+
let outcome: ResultTest['outcome']
|
|
456
|
+
let reason: string | null
|
|
457
|
+
if (input.outcome === undefined) {
|
|
458
|
+
requireCondition(input.reason === undefined, '--reason applies only with --outcome')
|
|
459
|
+
requireCondition(!input.evidence?.length, '--evidence applies only with --outcome error')
|
|
460
|
+
const missing = test.assertions.filter((assertion) => !results.has(`${test.id}.${assertion.id}`))
|
|
461
|
+
requireCondition(
|
|
462
|
+
!missing.length,
|
|
463
|
+
`Assertions without a result: ${missing.map((assertion) => `${test.id}.${assertion.id}`).join(', ')}`
|
|
464
|
+
)
|
|
465
|
+
outcome = testOutcome(recorded.map((row) => row.result))
|
|
466
|
+
const unchecked = recorded.filter((row) => row.result === 'not_checked')
|
|
467
|
+
reason =
|
|
468
|
+
outcome === 'error'
|
|
469
|
+
? `Assertions not checked: ${unchecked.map((row) => `${test.id}.${row.assertion_id}`).join(', ')}`
|
|
470
|
+
: null
|
|
471
|
+
} else {
|
|
472
|
+
outcome = requireChoice(input.outcome, ['blocked', 'not_run', 'error'] as const, 'outcome (--outcome)')
|
|
473
|
+
reason = requireText(input.reason, `reason (--reason) for outcome ${outcome}`)
|
|
474
|
+
requireCondition(
|
|
475
|
+
outcome === 'error' || !recorded.length,
|
|
476
|
+
`Test ${test.id} has assertion results. Record the remaining assertions, or close it as error.`
|
|
477
|
+
)
|
|
478
|
+
requireCondition(
|
|
479
|
+
outcome === 'error' || !input.evidence?.length,
|
|
480
|
+
'--evidence applies only with --outcome error'
|
|
481
|
+
)
|
|
482
|
+
files = evidenceFiles(store, input.runId, test.id, input.evidence)
|
|
483
|
+
}
|
|
484
|
+
execute(
|
|
485
|
+
store,
|
|
486
|
+
'INSERT INTO outcomes (run_id, test_id, attempt, outcome, reason, recorded_at, operation_id) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
|
487
|
+
input.runId,
|
|
488
|
+
test.id,
|
|
489
|
+
currentAttempt(store, input.runId),
|
|
490
|
+
outcome,
|
|
491
|
+
reason,
|
|
492
|
+
at,
|
|
493
|
+
operationId
|
|
494
|
+
)
|
|
495
|
+
const evidenceIds = storeEvidence(store, input.runId, test.id, null, files, operationId)
|
|
496
|
+
return {
|
|
497
|
+
reply: completed(input.runId, { runId: input.runId, testId: test.id, outcome, reason, evidenceIds }),
|
|
498
|
+
subjects: [`run:${input.runId}`],
|
|
499
|
+
}
|
|
500
|
+
})
|
|
501
|
+
removeFiles(files)
|
|
502
|
+
return reply
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export function recordFinding(store: Store, actor: string | undefined, input: { runId: string; finding: unknown }) {
|
|
506
|
+
return write(store, { actor, command: 'run finding', subject: input.runId, input }, (operationId) => {
|
|
507
|
+
const run = startedRun(store, input.runId)
|
|
508
|
+
const finding = validate('finding', input.finding)
|
|
509
|
+
requireCondition(
|
|
510
|
+
!one(store, 'SELECT 1 FROM findings WHERE run_id = ? AND id = ?', input.runId, finding.id),
|
|
511
|
+
`Finding ${finding.id} exists in run ${input.runId}`
|
|
512
|
+
)
|
|
513
|
+
const results = runAssertions(store, input.runId)
|
|
514
|
+
const named = finding.assertionIds.map((reference) => {
|
|
515
|
+
const [testId, assertionId] = reference.split('.')
|
|
516
|
+
const test = selectedTest(run, testId)
|
|
517
|
+
requireCondition(
|
|
518
|
+
test.assertions.some((assertion) => assertion.id === assertionId),
|
|
519
|
+
`Unknown assertion in finding ${finding.id}: ${reference}`
|
|
520
|
+
)
|
|
521
|
+
if (finding.type === 'defect')
|
|
522
|
+
requireCondition(
|
|
523
|
+
results.get(reference)?.result === 'failed',
|
|
524
|
+
`Defect ${finding.id} names ${reference}, which is not failed. A defect names only failed assertions.`
|
|
525
|
+
)
|
|
526
|
+
return { test, assertionId }
|
|
527
|
+
})
|
|
528
|
+
const testIds = run.plan.tests
|
|
529
|
+
.filter((test) => named.some((item) => item.test.id === test.id))
|
|
530
|
+
.map((test) => test.id)
|
|
531
|
+
for (const requirementId of finding.requirementIds)
|
|
532
|
+
for (const { test } of named)
|
|
533
|
+
requireCondition(
|
|
534
|
+
test.requirementIds.includes(requirementId),
|
|
535
|
+
`Finding ${finding.id} names requirement ${requirementId}, which test ${test.id} does not cover`
|
|
536
|
+
)
|
|
537
|
+
const evidence = new Map(runEvidence(store, input.runId).map((row) => [row.id, row]))
|
|
538
|
+
for (const id of finding.evidenceIds) {
|
|
539
|
+
const row = evidence.get(id)
|
|
540
|
+
requireCondition(row, `Unknown evidence in finding ${finding.id}: ${id}`)
|
|
541
|
+
requireCondition(
|
|
542
|
+
testIds.includes(row.test_id) &&
|
|
543
|
+
(row.assertion_id === null || finding.assertionIds.includes(`${row.test_id}.${row.assertion_id}`)),
|
|
544
|
+
`Evidence ${id} does not belong to the assertions or tests of finding ${finding.id}`
|
|
545
|
+
)
|
|
546
|
+
}
|
|
547
|
+
execute(
|
|
548
|
+
store,
|
|
549
|
+
'INSERT INTO findings (id, run_id, body, operation_id) VALUES (?, ?, ?, ?)',
|
|
550
|
+
finding.id,
|
|
551
|
+
input.runId,
|
|
552
|
+
canonical({ ...finding, testIds }),
|
|
553
|
+
operationId
|
|
554
|
+
)
|
|
555
|
+
return {
|
|
556
|
+
reply: completed(input.runId, { runId: input.runId, findingId: finding.id, testIds }),
|
|
557
|
+
subjects: [`run:${input.runId}`],
|
|
558
|
+
}
|
|
559
|
+
})
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
export function recordGap(store: Store, actor: string | undefined, input: { runId: string; gap: unknown }) {
|
|
563
|
+
return write(store, { actor, command: 'run gap', subject: input.runId, input }, (operationId) => {
|
|
564
|
+
const run = startedRun(store, input.runId)
|
|
565
|
+
const gap = validate('gap', input.gap)
|
|
566
|
+
requireCondition(
|
|
567
|
+
gap.status === 'open' && gap.decisionId === null,
|
|
568
|
+
`Runtime gap ${gap.id} must be open and have no decision`
|
|
569
|
+
)
|
|
570
|
+
requireCondition(
|
|
571
|
+
!run.plan.gaps.some((old) => old.id === gap.id),
|
|
572
|
+
`Gap ${gap.id} exists in ${run.plan.planId}@${run.plan.revision}. Use a new gap ID.`
|
|
573
|
+
)
|
|
574
|
+
requireCondition(
|
|
575
|
+
!one(store, 'SELECT 1 FROM run_gaps WHERE run_id = ? AND id = ?', input.runId, gap.id),
|
|
576
|
+
`Gap ${gap.id} exists in run ${input.runId}. Use a new gap ID.`
|
|
577
|
+
)
|
|
578
|
+
for (const id of gap.requirementIds)
|
|
579
|
+
requireCondition(
|
|
580
|
+
run.plan.requirements.some((requirement) => requirement.id === id),
|
|
581
|
+
`Unknown requirement in gap ${gap.id}: ${id}`
|
|
582
|
+
)
|
|
583
|
+
const plans = latestPlans(store)
|
|
584
|
+
for (const reference of gap.testIds) {
|
|
585
|
+
const { planId, testId } = testReference(reference)
|
|
586
|
+
const plan = planId === null ? run.plan : plans.get(planId)
|
|
587
|
+
requireCondition(
|
|
588
|
+
plan?.tests.some((test) => test.id === testId),
|
|
589
|
+
`Unknown test in gap ${gap.id}: ${reference}`
|
|
590
|
+
)
|
|
591
|
+
}
|
|
592
|
+
execute(
|
|
593
|
+
store,
|
|
594
|
+
'INSERT INTO run_gaps (id, run_id, body, operation_id) VALUES (?, ?, ?, ?)',
|
|
595
|
+
gap.id,
|
|
596
|
+
input.runId,
|
|
597
|
+
canonical(gap),
|
|
598
|
+
operationId
|
|
599
|
+
)
|
|
600
|
+
return {
|
|
601
|
+
reply: completed(input.runId, { runId: input.runId, gapId: gap.id }),
|
|
602
|
+
subjects: [`run:${input.runId}`],
|
|
603
|
+
}
|
|
604
|
+
})
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
export function finishRun(store: Store, actor: string | undefined, input: FinishInput) {
|
|
608
|
+
const reply = finish(store, actor, input)
|
|
609
|
+
removeTemporaryDirectory(store, input.runId)
|
|
610
|
+
return reply
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function finish(store: Store, actor: string | undefined, input: FinishInput) {
|
|
614
|
+
return write(store, { actor, command: 'run finish', subject: input.runId, input }, (operationId, at) => {
|
|
615
|
+
const run = startedRun(store, input.runId)
|
|
616
|
+
const completion = requireChoice(
|
|
617
|
+
input.completion,
|
|
618
|
+
['completed', 'interrupted', 'operational_error'] as const,
|
|
619
|
+
'completion (--completion)'
|
|
620
|
+
)
|
|
621
|
+
const cleanup = {
|
|
622
|
+
status: requireChoice(
|
|
623
|
+
input.cleanup,
|
|
624
|
+
['completed', 'failed', 'not_applicable'] as const,
|
|
625
|
+
'cleanup (--cleanup)'
|
|
626
|
+
),
|
|
627
|
+
reason: requireText(input.cleanupReason, 'cleanup reason (--cleanup-reason)'),
|
|
628
|
+
}
|
|
629
|
+
const selected = run.request.selectedTestIds
|
|
630
|
+
const attempt = currentAttempt(store, input.runId)
|
|
631
|
+
const unclosed = selected.filter((id) => !runOutcomes(store, input.runId).has(id))
|
|
632
|
+
if (completion === 'completed')
|
|
633
|
+
requireCondition(
|
|
634
|
+
!unclosed.length,
|
|
635
|
+
`Selected tests are not closed: ${unclosed.join(', ')}. Close them, or finish as interrupted.`
|
|
636
|
+
)
|
|
637
|
+
for (const testId of unclosed)
|
|
638
|
+
execute(
|
|
639
|
+
store,
|
|
640
|
+
'INSERT INTO outcomes (run_id, test_id, attempt, outcome, reason, recorded_at, operation_id) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
|
641
|
+
input.runId,
|
|
642
|
+
testId,
|
|
643
|
+
attempt,
|
|
644
|
+
'error',
|
|
645
|
+
stoppedReason,
|
|
646
|
+
at,
|
|
647
|
+
operationId
|
|
648
|
+
)
|
|
649
|
+
const outcomes = runOutcomes(store, input.runId)
|
|
650
|
+
const results = runAssertions(store, input.runId)
|
|
651
|
+
const findings = runFindings(store, input.runId)
|
|
652
|
+
const newGaps = runGaps(store, input.runId)
|
|
653
|
+
for (const row of results.values())
|
|
654
|
+
if (row.result === 'failed')
|
|
655
|
+
requireCondition(
|
|
656
|
+
findings.some(
|
|
657
|
+
(finding) =>
|
|
658
|
+
finding.type === 'defect' &&
|
|
659
|
+
finding.assertionIds.includes(`${row.test_id}.${row.assertion_id}`)
|
|
660
|
+
),
|
|
661
|
+
`Failed assertion ${row.test_id}.${row.assertion_id} needs a defect finding. Record it with run finding.`
|
|
662
|
+
)
|
|
663
|
+
if (completion === 'completed') {
|
|
664
|
+
const failures = selected.filter((id) => outcomes.get(id)!.outcome === 'error')
|
|
665
|
+
requireCondition(
|
|
666
|
+
!failures.length,
|
|
667
|
+
`Operational failure cannot be completed: ${failures.join(', ')} closed as error`
|
|
668
|
+
)
|
|
669
|
+
requireCondition(cleanup.status !== 'failed', 'Cleanup failed, so the run cannot be completed')
|
|
670
|
+
for (const test of run.plan.tests.filter((test) => selected.includes(test.id))) {
|
|
671
|
+
const outcome = outcomes.get(test.id)!.outcome
|
|
672
|
+
if (outcome !== 'not_run' && outcome !== 'blocked') continue
|
|
673
|
+
const cause =
|
|
674
|
+
test.dependsOn.some((id) =>
|
|
675
|
+
id.includes(':')
|
|
676
|
+
? dependencyBlock(store, id, run.row.application_build) !== null
|
|
677
|
+
: outcomes.get(id)?.outcome !== 'passed'
|
|
678
|
+
) || newGaps.some((gap) => gap.severity === 'blocking')
|
|
679
|
+
requireCondition(
|
|
680
|
+
cause,
|
|
681
|
+
`Selected test ${test.id} is ${outcome} without a cause. A completed run needs a failed prerequisite or a new blocking gap.`
|
|
682
|
+
)
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
const evidence = runEvidence(store, input.runId)
|
|
686
|
+
const tests: ResultTest[] = run.plan.tests.map((test) => {
|
|
687
|
+
const closed = outcomes.get(test.id)
|
|
688
|
+
if (!closed)
|
|
689
|
+
return { testId: test.id, outcome: 'not_run', reason: 'Test was not selected.', assertions: [] }
|
|
690
|
+
return {
|
|
691
|
+
testId: test.id,
|
|
692
|
+
outcome: closed.outcome,
|
|
693
|
+
reason: closed.reason,
|
|
694
|
+
assertions: test.assertions.flatMap((assertion) => {
|
|
695
|
+
const row = results.get(`${test.id}.${assertion.id}`)
|
|
696
|
+
if (!row) return []
|
|
697
|
+
return {
|
|
698
|
+
assertionId: assertion.id,
|
|
699
|
+
check: assertion.check,
|
|
700
|
+
checkKind: assertion.checkKind,
|
|
701
|
+
result: row.result,
|
|
702
|
+
observed: row.observed,
|
|
703
|
+
reason: row.reason,
|
|
704
|
+
evidenceIds: evidence
|
|
705
|
+
.filter((item) => item.test_id === test.id && item.assertion_id === assertion.id)
|
|
706
|
+
.map((item) => item.id),
|
|
707
|
+
}
|
|
708
|
+
}),
|
|
709
|
+
}
|
|
710
|
+
})
|
|
711
|
+
const coverage = resultCoverage(run.plan, tests, newGaps)
|
|
712
|
+
const result: Result = validate('result', {
|
|
713
|
+
contractVersion: 'toolkit-2',
|
|
714
|
+
runId: input.runId,
|
|
715
|
+
plan: { planId: run.plan.planId, revision: run.plan.revision, sha256: run.row.plan_sha256 },
|
|
716
|
+
target: run.plan.target,
|
|
717
|
+
applicationBuild: run.row.application_build,
|
|
718
|
+
selectedTestIds: selected,
|
|
719
|
+
retestOf: run.request.retestOf,
|
|
720
|
+
startedAt: run.row.started_at,
|
|
721
|
+
finishedAt: at,
|
|
722
|
+
attempts: attempt,
|
|
723
|
+
completion,
|
|
724
|
+
tests,
|
|
725
|
+
findings,
|
|
726
|
+
newGaps,
|
|
727
|
+
evidence: evidence.map((row) => ({
|
|
728
|
+
id: row.id,
|
|
729
|
+
testId: row.test_id,
|
|
730
|
+
assertionId: row.assertion_id,
|
|
731
|
+
kind: row.kind,
|
|
732
|
+
name: row.name,
|
|
733
|
+
mediaType: row.media_type,
|
|
734
|
+
sha256: row.sha256,
|
|
735
|
+
size: row.size,
|
|
736
|
+
})),
|
|
737
|
+
cleanup,
|
|
738
|
+
coverage,
|
|
739
|
+
})
|
|
740
|
+
const body = canonical(result)
|
|
741
|
+
const sha256 = digest(body)
|
|
742
|
+
execute(
|
|
743
|
+
store,
|
|
744
|
+
'INSERT INTO results (run_id, completion, coverage, finished_at, sha256, body, operation_id) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
|
745
|
+
input.runId,
|
|
746
|
+
completion,
|
|
747
|
+
coverage.status,
|
|
748
|
+
at,
|
|
749
|
+
sha256,
|
|
750
|
+
body,
|
|
751
|
+
operationId
|
|
752
|
+
)
|
|
753
|
+
return {
|
|
754
|
+
reply: {
|
|
755
|
+
status: completion,
|
|
756
|
+
subject: input.runId,
|
|
757
|
+
reasons: [],
|
|
758
|
+
data: {
|
|
759
|
+
runId: input.runId,
|
|
760
|
+
completion,
|
|
761
|
+
coverage: coverage.status,
|
|
762
|
+
sha256,
|
|
763
|
+
counts: outcomeCounts(tests.filter((test) => selected.includes(test.testId))),
|
|
764
|
+
},
|
|
765
|
+
},
|
|
766
|
+
subjects: [`run:${input.runId}`, `plan:${run.plan.planId}`, 'inbox'],
|
|
767
|
+
}
|
|
768
|
+
})
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
export function outcomeCounts(tests: ResultTest[]) {
|
|
772
|
+
const counts: Record<string, number> = {}
|
|
773
|
+
for (const test of tests) counts[test.outcome] = (counts[test.outcome] ?? 0) + 1
|
|
774
|
+
return counts
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
export function resumeRun(store: Store, actor: string | undefined, input: ResumeInput) {
|
|
778
|
+
return write(store, { actor, command: 'run resume', subject: input.runId, input }, (operationId, at, actor) => {
|
|
779
|
+
const run = startedRun(store, input.runId)
|
|
780
|
+
const stalled = input.error ?? []
|
|
781
|
+
requireCondition(stalled.length > 0 || input.reason === undefined, '--reason applies only with --error')
|
|
782
|
+
const reason = stalled.length ? requireText(input.reason, 'reason (--reason) for --error') : null
|
|
783
|
+
const attempt = currentAttempt(store, input.runId) + 1
|
|
784
|
+
execute(
|
|
785
|
+
store,
|
|
786
|
+
'INSERT INTO run_attempts (run_id, attempt, started_at, actor) VALUES (?, ?, ?, ?)',
|
|
787
|
+
input.runId,
|
|
788
|
+
attempt,
|
|
789
|
+
at,
|
|
790
|
+
actor
|
|
791
|
+
)
|
|
792
|
+
for (const testId of stalled) {
|
|
793
|
+
selectedTest(run, testId)
|
|
794
|
+
requireOpen(store, input.runId, testId)
|
|
795
|
+
execute(
|
|
796
|
+
store,
|
|
797
|
+
'INSERT INTO outcomes (run_id, test_id, attempt, outcome, reason, recorded_at, operation_id) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
|
798
|
+
input.runId,
|
|
799
|
+
testId,
|
|
800
|
+
attempt,
|
|
801
|
+
'error',
|
|
802
|
+
reason,
|
|
803
|
+
at,
|
|
804
|
+
operationId
|
|
805
|
+
)
|
|
806
|
+
}
|
|
807
|
+
const outcomes = runOutcomes(store, input.runId)
|
|
808
|
+
const results = runAssertions(store, input.runId)
|
|
809
|
+
const tests = run.plan.tests
|
|
810
|
+
.filter((test) => run.request.selectedTestIds.includes(test.id) && !outcomes.has(test.id))
|
|
811
|
+
.map((test) => ({
|
|
812
|
+
testId: test.id,
|
|
813
|
+
warning: 'Actions may have occurred',
|
|
814
|
+
recorded: test.assertions.filter((assertion) => results.has(`${test.id}.${assertion.id}`)).length,
|
|
815
|
+
assertions: test.assertions.filter((assertion) => !results.has(`${test.id}.${assertion.id}`)),
|
|
816
|
+
}))
|
|
817
|
+
return {
|
|
818
|
+
reply: completed(input.runId, {
|
|
819
|
+
runId: input.runId,
|
|
820
|
+
attempt,
|
|
821
|
+
browserSession: run.row.browser_session,
|
|
822
|
+
deadlineSeconds: run.row.deadline_seconds,
|
|
823
|
+
evidenceDirectory: evidenceDirectory(input.runId),
|
|
824
|
+
closed: stalled,
|
|
825
|
+
tests,
|
|
826
|
+
}),
|
|
827
|
+
subjects: [`run:${input.runId}`],
|
|
828
|
+
}
|
|
829
|
+
})
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
export function acceptResult(store: Store, actor: string | undefined, input: ResultVerdict) {
|
|
833
|
+
return decideResult(store, actor, input, 'approved')
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
export function rejectResult(store: Store, actor: string | undefined, input: ResultVerdict) {
|
|
837
|
+
return decideResult(store, actor, input, 'rejected')
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function decideResult(store: Store, actor: string | undefined, input: ResultVerdict, verdict: 'approved' | 'rejected') {
|
|
841
|
+
const command = verdict === 'approved' ? 'result accept' : 'result reject'
|
|
842
|
+
return write(store, { actor, command, subject: input.runId, input }, (operationId, at, actor) => {
|
|
843
|
+
const run = runState(store, input.runId)
|
|
844
|
+
const result = runResult(store, input.runId)
|
|
845
|
+
requireCondition(result, `Run ${input.runId} is not finished. Finish it before you decide on its result.`)
|
|
846
|
+
expectDigest(result.sha256, input.expectSha256, input.runId)
|
|
847
|
+
return {
|
|
848
|
+
reply: recordApproval(store, actor, operationId, {
|
|
849
|
+
kind: 'result',
|
|
850
|
+
subject: input.runId,
|
|
851
|
+
sha256: result.sha256,
|
|
852
|
+
verdict,
|
|
853
|
+
rationale: input.rationale,
|
|
854
|
+
}),
|
|
855
|
+
subjects: [`run:${input.runId}`, `plan:${run.plan.planId}`, 'inbox'],
|
|
856
|
+
}
|
|
857
|
+
})
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
export function runResult(store: Store, runId: string) {
|
|
861
|
+
const row = one<{ sha256: string; body: string }>(store, 'SELECT sha256, body FROM results WHERE run_id = ?', runId)
|
|
862
|
+
return row ? { sha256: row.sha256, result: JSON.parse(row.body) as Result } : null
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
export function resultAcceptance(store: Store, run: RunRow, sha256: string) {
|
|
866
|
+
const policy = approvalPolicy(targetRevision(store, run.target_id, run.target_revision).profile).result
|
|
867
|
+
return approvalState(store, 'result', run.run_id, sha256, policy)
|
|
868
|
+
}
|