@open-mercato/cli 0.6.7 → 0.6.8-canary.319.1.5b4be4bc52
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/.turbo/turbo-build.log +77 -4
- package/build.mjs +68 -11
- package/dist/agentic/claude-code/hooks/entity-migration-check.ts +32 -88
- package/dist/agentic/codex/enforcement-rules.md +2 -16
- package/dist/agentic/cursor/hooks/entity-migration-check.mjs +19 -50
- package/dist/agentic/cursor/rules/entity-guard.mdc +9 -21
- package/dist/agentic/cursor/rules/generated-guard.mdc +3 -5
- package/dist/agentic/cursor/rules/open-mercato.mdc +3 -29
- package/dist/agentic/guides/ai-workflows.md +76 -0
- package/dist/agentic/guides/architecture.md +107 -0
- package/dist/agentic/guides/backend-ui.md +84 -0
- package/dist/agentic/guides/contracts.md +92 -0
- package/dist/agentic/guides/extensions.md +121 -0
- package/dist/agentic/guides/framework-extension-points.md +36 -0
- package/dist/agentic/guides/integrations.md +81 -0
- package/dist/agentic/guides/module-facts.json +116757 -3951
- package/dist/agentic/guides/module-system.md +12 -127
- package/dist/agentic/guides/modules/ai_assistant.md +227 -28
- package/dist/agentic/guides/modules/api_docs.md +58 -2
- package/dist/agentic/guides/modules/api_keys.md +92 -7
- package/dist/agentic/guides/modules/attachments.md +119 -14
- package/dist/agentic/guides/modules/audit_logs.md +131 -10
- package/dist/agentic/guides/modules/auth.md +276 -33
- package/dist/agentic/guides/modules/business_rules.md +149 -14
- package/dist/agentic/guides/modules/catalog.md +482 -43
- package/dist/agentic/guides/modules/channel_gmail.md +82 -3
- package/dist/agentic/guides/modules/channel_imap.md +82 -3
- package/dist/agentic/guides/modules/checkout.md +305 -27
- package/dist/agentic/guides/modules/communication_channels.md +315 -33
- package/dist/agentic/guides/modules/configs.md +104 -8
- package/dist/agentic/guides/modules/content.md +57 -2
- package/dist/agentic/guides/modules/currencies.md +152 -17
- package/dist/agentic/guides/modules/customer_accounts.md +387 -45
- package/dist/agentic/guides/modules/customers.md +746 -85
- package/dist/agentic/guides/modules/dashboards.md +155 -10
- package/dist/agentic/guides/modules/data_sync.md +149 -16
- package/dist/agentic/guides/modules/design_system.md +102 -0
- package/dist/agentic/guides/modules/dictionaries.md +107 -12
- package/dist/agentic/guides/modules/directory.md +148 -15
- package/dist/agentic/guides/modules/entities.md +145 -13
- package/dist/agentic/guides/modules/events.md +59 -2
- package/dist/agentic/guides/modules/feature_toggles.md +119 -9
- package/dist/agentic/guides/modules/gateway_stripe.md +92 -4
- package/dist/agentic/guides/modules/generators.md +45 -2
- package/dist/agentic/guides/modules/inbox_ops.md +207 -27
- package/dist/agentic/guides/modules/integrations.md +149 -16
- package/dist/agentic/guides/modules/messages.md +223 -24
- package/dist/agentic/guides/modules/notifications.md +96 -8
- package/dist/agentic/guides/modules/onboarding.md +77 -6
- package/dist/agentic/guides/modules/payment_gateways.md +168 -21
- package/dist/agentic/guides/modules/perspectives.md +77 -7
- package/dist/agentic/guides/modules/planner.md +136 -18
- package/dist/agentic/guides/modules/portal.md +93 -2
- package/dist/agentic/guides/modules/progress.md +104 -14
- package/dist/agentic/guides/modules/query_index.md +154 -15
- package/dist/agentic/guides/modules/record_locks.md +206 -21
- package/dist/agentic/guides/modules/resources.md +197 -30
- package/dist/agentic/guides/modules/sales.md +687 -84
- package/dist/agentic/guides/modules/scheduler.md +133 -14
- package/dist/agentic/guides/modules/search.md +170 -6
- package/dist/agentic/guides/modules/security.md +294 -35
- package/dist/agentic/guides/modules/shipping_carriers.md +134 -18
- package/dist/agentic/guides/modules/sso.md +151 -26
- package/dist/agentic/guides/modules/staff.md +372 -57
- package/dist/agentic/guides/modules/storage_s3.md +95 -5
- package/dist/agentic/guides/modules/sync_akeneo.md +85 -3
- package/dist/agentic/guides/modules/sync_excel.md +75 -6
- package/dist/agentic/guides/modules/system_status_overlays.md +60 -2
- package/dist/agentic/guides/modules/translations.md +104 -10
- package/dist/agentic/guides/modules/webhooks.md +203 -24
- package/dist/agentic/guides/modules/wms.md +469 -47
- package/dist/agentic/guides/modules/workflows.md +302 -43
- package/dist/agentic/guides/reference-module-facts.json +6370 -0
- package/dist/agentic/guides/reference-modules/example.md +451 -0
- package/dist/agentic/guides/spec-delivery.md +42 -0
- package/dist/agentic/guides/testing-debugging.md +80 -0
- package/dist/agentic/guides/upstream/AGENTS.md +299 -0
- package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +316 -0
- package/dist/agentic/guides/upstream/manifest.json +8 -0
- package/dist/agentic/shared/AGENTS.md.template +120 -311
- package/dist/agentic/shared/ai/agentic.config.json +2 -0
- package/dist/agentic/shared/ai/harness/README.md +50 -0
- package/dist/agentic/shared/ai/harness/RELEASE.md +57 -0
- package/dist/agentic/shared/ai/harness/cases.json +15050 -0
- package/dist/agentic/shared/ai/harness/cases.schema.json +188 -0
- package/dist/agentic/shared/ai/harness/fixtures/index.json +51 -0
- package/dist/agentic/shared/ai/harness/fixtures/seeds.json +166 -0
- package/dist/agentic/shared/ai/harness/generated-code-review-policy.md +21 -0
- package/dist/agentic/shared/ai/harness/generated-code-review-response.schema.json +96 -0
- package/dist/agentic/shared/ai/harness/generated-code-review-result.schema.json +172 -0
- package/dist/agentic/shared/ai/harness/knowledge-change.schema.json +228 -0
- package/dist/agentic/shared/ai/harness/release-matrix.json +163 -0
- package/dist/agentic/shared/ai/harness/release-result.schema.json +219 -0
- package/dist/agentic/shared/ai/harness/result.schema.json +109 -0
- package/dist/agentic/shared/ai/harness/routing-response.schema.json +22 -0
- package/dist/agentic/shared/ai/harness/target-validation-result.schema.json +73 -0
- package/dist/agentic/shared/ai/harness/validators.json +82 -0
- package/dist/agentic/shared/ai/harness/writable-ast-oracles.mjs +2121 -0
- package/dist/agentic/shared/ai/harness/writable-behavior-oracles.mjs +1096 -0
- package/dist/agentic/shared/ai/harness/writable-spec-oracles.mjs +412 -0
- package/dist/agentic/shared/ai/lessons/_template.md +16 -0
- package/dist/agentic/shared/ai/lessons.md +26 -3
- package/dist/agentic/shared/ai/review-checklist.md +36 -0
- package/dist/agentic/shared/ai/skills/om-auto-implement-spec/SKILL.md +21 -0
- package/dist/agentic/shared/ai/skills/om-auto-review-pr/SKILL.md +4 -0
- package/dist/agentic/shared/ai/skills/om-auto-upgrade-0.6.6-to-0.6.7/SKILL.md +98 -0
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/SKILL.md +21 -241
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/crud-surfaces.md +31 -0
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/frontend-and-design-system.md +53 -0
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/page-and-navigation.md +15 -0
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/quality-states.md +11 -0
- package/dist/agentic/shared/ai/skills/om-build-workflow/SKILL.md +30 -0
- package/dist/agentic/shared/ai/skills/om-build-workflow/references/activity-contracts.md +12 -0
- package/dist/agentic/shared/ai/skills/om-build-workflow/references/durability-and-progress.md +13 -0
- package/dist/agentic/shared/ai/skills/om-build-workflow/references/workflow-design.md +14 -0
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/SKILL.md +28 -0
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/attachments-and-overrides.md +12 -0
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/module-agents-and-tools.md +22 -0
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/orchestrator-agents.md +12 -0
- package/dist/agentic/shared/ai/skills/om-create-ai-agent/references/surface-selector.md +19 -0
- package/dist/agentic/shared/ai/skills/om-data-model-design/SKILL.md +17 -601
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/integrity-and-concurrency.md +21 -0
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/migration-workflow.md +15 -0
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/schema-design.md +16 -0
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/sensitive-data.md +10 -0
- package/dist/agentic/shared/ai/skills/om-eject-and-customize/SKILL.md +15 -310
- package/dist/agentic/shared/ai/skills/om-eject-and-customize/references/decision-and-procedure.md +19 -0
- package/dist/agentic/shared/ai/skills/om-evolve-harness/SKILL.md +31 -0
- package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-template.md +100 -0
- package/dist/agentic/shared/ai/skills/om-evolve-harness/references/case-workflow.md +16 -0
- package/dist/agentic/shared/ai/skills/om-evolve-harness/references/knowledge-change.md +115 -0
- package/dist/agentic/shared/ai/skills/om-evolve-harness/references/owner-selection.md +17 -0
- package/dist/agentic/shared/ai/skills/om-framework-context/SKILL.md +25 -0
- package/dist/agentic/shared/ai/skills/om-framework-context/references/resolver-procedure.md +11 -0
- package/dist/agentic/shared/ai/skills/om-framework-context/references/skew-and-escalation.md +13 -0
- package/dist/agentic/shared/ai/skills/om-help/SKILL.md +15 -97
- package/dist/agentic/shared/ai/skills/om-help/references/delivery-workflows.md +18 -0
- package/dist/agentic/shared/ai/skills/om-help/references/task-families.md +20 -0
- package/dist/agentic/shared/ai/skills/om-implement-spec/SKILL.md +33 -152
- package/dist/agentic/shared/ai/skills/om-implement-spec/references/phases-and-gates.md +33 -0
- package/dist/agentic/shared/ai/skills/om-implement-spec/references/planning-and-progress.md +40 -0
- package/dist/agentic/shared/ai/skills/om-implement-spec/references/report-templates.md +27 -0
- package/dist/agentic/shared/ai/skills/om-implement-spec/references/spec-resolution.md +24 -0
- package/dist/agentic/shared/ai/skills/om-integration-builder/SKILL.md +25 -708
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/package-and-activation.md +16 -0
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/provider-families.md +25 -0
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/security-and-reliability.md +15 -0
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/sync-and-files.md +12 -0
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/SKILL.md +34 -0
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/agents/openai.yaml +4 -0
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/agentic-setup.md +11 -0
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/input-normalization.md +31 -0
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/judge-workflow.md +39 -0
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/report-template.md +43 -0
- package/dist/agentic/shared/ai/skills/om-judge-agent-session/references/rules.md +11 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/SKILL.md +20 -820
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/api-and-domain.md +32 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/business-one-shot-blueprints.md +172 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/data-and-migrations.md +14 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/discovery-surface-catalog.md +83 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/module-surfaces.md +40 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/planning.md +11 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/runtime-cache-and-queues.md +22 -0
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/verification.md +13 -0
- package/dist/agentic/shared/ai/skills/om-share-this-session/SKILL.md +41 -0
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/agentic-setup.md +19 -0
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/bundle-preparation.md +44 -0
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/consent-and-review.md +39 -0
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/publication.md +20 -0
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/report-templates.md +57 -0
- package/dist/agentic/shared/ai/skills/om-share-this-session/references/rules.md +11 -0
- package/dist/agentic/shared/ai/skills/om-share-this-session/scripts/prepare-share-bundle.mjs +486 -0
- package/dist/agentic/shared/ai/skills/om-system-extension/SKILL.md +20 -843
- package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-branches.md +42 -0
- package/dist/agentic/shared/ai/skills/om-system-extension/references/mechanism-selector.md +27 -0
- package/dist/agentic/shared/ai/skills/om-system-extension/references/read-write-roundtrip.md +13 -0
- package/dist/agentic/shared/ai/skills/om-system-extension/references/unified-overrides.md +43 -0
- package/dist/agentic/shared/ai/skills/om-system-extension/references/verification.md +11 -0
- package/dist/agentic/shared/ai/skills/om-trim-unused-modules/SKILL.md +15 -63
- package/dist/agentic/shared/ai/skills/om-trim-unused-modules/references/dependency-and-redirect.md +15 -0
- package/dist/agentic/shared/ai/skills/om-troubleshooter/SKILL.md +17 -467
- package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnosis-map.md +29 -0
- package/dist/agentic/shared/ai/skills/om-troubleshooter/references/regression-oracles.md +14 -0
- package/dist/agentic/shared/ai/skills/tiers.json +143 -21
- package/dist/agentic/shared/ai/skills/tiers.schema.json +52 -40
- package/dist/agentic/shared/ai/specs/README.md +4 -3
- package/dist/agentic/shared/ai/specs/SPEC-000-template.md +226 -10
- package/dist/agentic/shared/ai/trackers/github.md +108 -4
- package/dist/agentic/shared/scripts/agent-harness-tool-server.mjs +224 -0
- package/dist/agentic/shared/scripts/check-lessons.mjs +178 -0
- package/dist/agentic/shared/scripts/evaluate-agent-harness.mjs +3585 -0
- package/dist/agentic/shared/scripts/execution-sandbox.mjs +329 -0
- package/dist/agentic/shared/scripts/framework-context.mjs +888 -0
- package/dist/agentic/shared/scripts/install-skills.mjs +1029 -0
- package/dist/agentic/shared/scripts/install-skills.sh +4 -694
- package/dist/agentic/shared/scripts/prepare-agent-harness-fixture.mjs +222 -0
- package/dist/agentic/shared/scripts/run-agent-harness-release.mjs +1774 -0
- package/dist/agentic/shared/scripts/validate-knowledge-change.mjs +911 -0
- package/dist/bin.js +13 -1
- package/dist/bin.js.map +2 -2
- package/dist/lib/__integration__/TC-INT-008.spec.js +88 -8
- package/dist/lib/__integration__/TC-INT-008.spec.js.map +2 -2
- package/dist/lib/agentic-init.js +13 -3
- package/dist/lib/agentic-init.js.map +2 -2
- package/dist/lib/agentic-setup.js +434 -186
- package/dist/lib/agentic-setup.js.map +2 -2
- package/dist/lib/generators/extensions/injection-widgets.js +20 -0
- package/dist/lib/generators/extensions/injection-widgets.js.map +2 -2
- package/dist/lib/generators/module-extension-facts.js +1872 -0
- package/dist/lib/generators/module-extension-facts.js.map +7 -0
- package/dist/lib/generators/module-fact-sources.js +70 -0
- package/dist/lib/generators/module-fact-sources.js.map +7 -0
- package/dist/lib/generators/module-facts-discovery.js +64 -7
- package/dist/lib/generators/module-facts-discovery.js.map +2 -2
- package/dist/lib/generators/module-facts.js +1629 -52
- package/dist/lib/generators/module-facts.js.map +3 -3
- package/dist/lib/generators/module-override-targets.js +688 -0
- package/dist/lib/generators/module-override-targets.js.map +7 -0
- package/dist/lib/generators/module-registry.js +11 -5
- package/dist/lib/generators/module-registry.js.map +2 -2
- package/dist/lib/load-env.js +24 -0
- package/dist/lib/load-env.js.map +7 -0
- package/dist/lib/resolver.js +2 -0
- package/dist/lib/resolver.js.map +2 -2
- package/dist/lib/telemetry-init.js +358 -0
- package/dist/lib/telemetry-init.js.map +7 -0
- package/dist/lib/testing/integration.js +5 -2
- package/dist/lib/testing/integration.js.map +2 -2
- package/dist/mercato.js +23 -1
- package/dist/mercato.js.map +2 -2
- package/package.json +13 -11
- package/src/bin.ts +33 -1
- package/src/lib/__integration__/TC-INT-008.spec.ts +107 -9
- package/src/lib/__tests__/agentic-init.test.ts +41 -5
- package/src/lib/__tests__/agentic-setup.ownership.test.ts +386 -0
- package/src/lib/__tests__/injection-widget-id-validation.test.ts +184 -0
- package/src/lib/__tests__/load-env.test.ts +65 -0
- package/src/lib/__tests__/telemetry-init.test.ts +298 -0
- package/src/lib/agentic-init.ts +15 -3
- package/src/lib/agentic-setup.ts +573 -222
- package/src/lib/generators/__tests__/activation-adapter-coverage.test.ts +92 -0
- package/src/lib/generators/__tests__/module-extension-activations.test.ts +762 -0
- package/src/lib/generators/__tests__/module-extension-facts.test.ts +603 -0
- package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +139 -2
- package/src/lib/generators/__tests__/module-facts.computed-di-token.test.ts +88 -0
- package/src/lib/generators/__tests__/module-facts.customers.fixture.test.ts +1 -0
- package/src/lib/generators/__tests__/module-facts.discovery.test.ts +54 -10
- package/src/lib/generators/__tests__/module-facts.extension-hosts.test.ts +108 -0
- package/src/lib/generators/__tests__/module-facts.local-reference.test.ts +391 -0
- package/src/lib/generators/__tests__/module-facts.owned-contracts.test.ts +561 -0
- package/src/lib/generators/__tests__/module-facts.search-entity-id.test.ts +151 -0
- package/src/lib/generators/__tests__/module-facts.source-links.test.ts +160 -0
- package/src/lib/generators/__tests__/module-override-targets.roundtrip.test.ts +149 -0
- package/src/lib/generators/__tests__/module-override-targets.test.ts +328 -0
- package/src/lib/generators/__tests__/module-subset.test.ts +18 -0
- package/src/lib/generators/extensions/injection-widgets.ts +51 -0
- package/src/lib/generators/module-extension-facts.ts +2433 -0
- package/src/lib/generators/module-fact-sources.ts +108 -0
- package/src/lib/generators/module-facts-discovery.ts +104 -7
- package/src/lib/generators/module-facts.ts +2387 -138
- package/src/lib/generators/module-override-targets.ts +863 -0
- package/src/lib/generators/module-registry.ts +16 -6
- package/src/lib/load-env.ts +30 -0
- package/src/lib/resolver.ts +3 -0
- package/src/lib/telemetry-init.ts +461 -0
- package/src/lib/testing/__tests__/integration.test.ts +4 -0
- package/src/lib/testing/integration.ts +5 -2
- package/src/mercato.ts +26 -1
- package/dist/agentic/guides/cache.md +0 -45
- package/dist/agentic/guides/core.md +0 -453
- package/dist/agentic/guides/events.md +0 -117
- package/dist/agentic/guides/queue.md +0 -53
- package/dist/agentic/guides/search.md +0 -115
- package/dist/agentic/guides/shared.md +0 -108
- package/dist/agentic/guides/ui.md +0 -347
- package/dist/agentic/shared/ai/skills/om-auto-continue-pr-loop/SKILL.md +0 -27
- package/dist/agentic/shared/ai/skills/om-auto-create-pr-loop/SKILL.md +0 -27
- package/dist/agentic/shared/ai/skills/om-backend-ui-design/references/ui-components.md +0 -260
- package/dist/agentic/shared/ai/skills/om-data-model-design/references/mikro-orm-cheatsheet.md +0 -146
- package/dist/agentic/shared/ai/skills/om-help/references/skills-catalog.md +0 -84
- package/dist/agentic/shared/ai/skills/om-help/references/workflow-sequences.md +0 -193
- package/dist/agentic/shared/ai/skills/om-integration-builder/STANDALONE.md +0 -34
- package/dist/agentic/shared/ai/skills/om-integration-builder/references/adapter-contracts.md +0 -762
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/naming-conventions.md +0 -50
- package/dist/agentic/shared/ai/skills/om-module-scaffold/references/navigation-patterns.md +0 -97
- package/dist/agentic/shared/ai/skills/om-system-extension/references/extension-contracts.md +0 -271
- package/dist/agentic/shared/ai/skills/om-troubleshooter/references/diagnostic-commands.md +0 -101
|
@@ -0,0 +1,1096 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from 'node:child_process'
|
|
4
|
+
import fs from 'node:fs'
|
|
5
|
+
import os from 'node:os'
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
import process from 'node:process'
|
|
8
|
+
import { createRequire } from 'node:module'
|
|
9
|
+
import vm from 'node:vm'
|
|
10
|
+
|
|
11
|
+
const EXIT_PASS = 0
|
|
12
|
+
const EXIT_FAILURE = 1
|
|
13
|
+
const EXIT_INVALID = 2
|
|
14
|
+
const MAX_SOURCE_BYTES = 256 * 1024
|
|
15
|
+
const WORKER_TIMEOUT_MS = 3_000
|
|
16
|
+
|
|
17
|
+
const CASES = {
|
|
18
|
+
'OMH-045': { file: 'src/modules/external_sync/lib/client.ts', allowCompiledImports: true },
|
|
19
|
+
'OMH-054': { file: 'src/modules/automation/workflows/call-api.ts' },
|
|
20
|
+
'OMH-057': { file: 'src/modules/harness_fixture/api/scope/route.ts' },
|
|
21
|
+
'OMH-060': { file: 'src/modules/harness_fixture/commands/update-record.ts' },
|
|
22
|
+
'OMH-061': {
|
|
23
|
+
file: 'src/modules/harness_fixture/backend/edit/page.tsx',
|
|
24
|
+
allowedCompiledImport: '@open-mercato/ui/backend/CrudForm',
|
|
25
|
+
},
|
|
26
|
+
'OMH-070': { file: 'src/modules/harness_fixture/workers/sync.ts' },
|
|
27
|
+
'OMH-093': { file: 'src/modules/customer_merge/commands/merge-contacts.ts', family: 'business-command', exportName: 'mergeContacts', requiredFlags: ['scopeValid', 'survivorSelected'] },
|
|
28
|
+
'OMH-105': { file: 'src/modules/deal_stages/commands/change-stage.ts', family: 'business-command', exportName: 'changeDealStage', requiredFlags: ['transitionAllowed', 'requiredFieldsPresent'], allowCompiledImports: true },
|
|
29
|
+
'OMH-107': { file: 'src/modules/quote_approval/commands/request-discount.ts', family: 'business-command', exportName: 'requestQuoteDiscount', requiredFlags: ['approvalSatisfied', 'separationOfDuties'] },
|
|
30
|
+
'OMH-115': { file: 'src/modules/deal_accessibility/lib/move-deal.ts', family: 'ui-business-surface', exportName: 'moveDealAccessibly', requiredFlags: ['accessGranted', 'keyboardEquivalent'] },
|
|
31
|
+
'OMH-122': { file: 'src/modules/stock_reservations/commands/reserve-stock.ts', family: 'business-command', exportName: 'reserveStock', requiredFlags: ['stockAvailable', 'reservationKeyPresent'] },
|
|
32
|
+
'OMH-128': { file: 'src/modules/bulk_pricing/commands/update-prices.ts', family: 'async-operation', exportName: 'updatePrices' },
|
|
33
|
+
'OMH-130': { file: 'src/modules/demo_requests/lib/submit-demo-request.ts', family: 'ui-business-surface', exportName: 'submitDemoRequest', requiredFlags: ['scopeDerived', 'consentAccepted'] },
|
|
34
|
+
'OMH-133': { file: 'src/modules/portal_quote_approval/commands/approve-quote.ts', family: 'business-command', exportName: 'approvePortalQuote', requiredFlags: ['portalScoped', 'latestVersion'] },
|
|
35
|
+
'OMH-137': { file: 'src/modules/setup_wizard/lib/advance-setup.ts', family: 'ui-business-surface', exportName: 'advanceSetupWizard', requiredFlags: ['draftPersisted', 'transitionAllowed'] },
|
|
36
|
+
'OMH-140': { file: 'src/modules/invoice_dunning/workflows/run-dunning.ts', family: 'async-operation', exportName: 'runInvoiceDunning' },
|
|
37
|
+
'OMH-144': { file: 'src/modules/quote_assistant/ai-tools.ts', family: 'ai-safe-agent', exportName: 'saveQuoteDraftWithApproval', mode: 'mutation' },
|
|
38
|
+
'OMH-146': { file: 'src/modules/sales_orchestrator/ai-agents.ts', family: 'ai-safe-agent', exportName: 'coordinateSalesQuestion', mode: 'delegate' },
|
|
39
|
+
'OMH-149': { file: 'src/modules/smtp_email/lib/client.ts', family: 'provider-adapter', exportName: 'sendTransactionalEmail' },
|
|
40
|
+
'OMH-150': { file: 'src/modules/card_payments/lib/client.ts', family: 'provider-adapter', exportName: 'createCardPayment' },
|
|
41
|
+
'OMH-151': { file: 'src/modules/carrier_shipping/lib/client.ts', family: 'provider-adapter', exportName: 'bookCarrierShipment' },
|
|
42
|
+
'OMH-153': { file: 'src/modules/erp_sync/data-sync.ts', family: 'data-flow', exportName: 'synchronizeErpPage' },
|
|
43
|
+
'OMH-156': { file: 'src/modules/product_transfer/lib/flow.ts', family: 'data-flow', exportName: 'transferProductRows' },
|
|
44
|
+
'OMH-171': { file: 'src/modules/harness_fixture/api/scope/route.ts', probeCase: 'OMH-057' },
|
|
45
|
+
'OMH-172': { file: 'src/modules/harness_fixture/backend/edit/page.tsx', probeCase: 'OMH-061', allowedCompiledImport: '@open-mercato/ui/backend/CrudForm' },
|
|
46
|
+
'OMH-181': { file: 'src/modules/order_risk/widgets/injection/order-risk-review/widget.ts', family: 'data-table-extension', exportName: 'reviewOrderRisk' },
|
|
47
|
+
'OMH-189': {
|
|
48
|
+
directory: 'src/modules/room_calendar_sync/lib',
|
|
49
|
+
file: 'src/modules/room_calendar_sync/lib/export-source.ts',
|
|
50
|
+
requiredExport: 'assertSafeEndpoint',
|
|
51
|
+
family: 'endpoint-guard',
|
|
52
|
+
allowCompiledImports: true,
|
|
53
|
+
},
|
|
54
|
+
'OMH-192': { file: 'src/modules/library/commands/crm-loans.ts', family: 'crm-library-regression' },
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const FAMILY_PROBES = {
|
|
58
|
+
'business-command': new vm.Script(`
|
|
59
|
+
'use strict';
|
|
60
|
+
globalThis.__oracleResult = (async () => {
|
|
61
|
+
const config = __oracleConfig
|
|
62
|
+
const action = module.exports[config.exportName]
|
|
63
|
+
if (typeof action !== 'function') throw new Error('required business command export is missing')
|
|
64
|
+
const checks = []
|
|
65
|
+
const validInput = { idempotencyKey: 'business-key', ...Object.fromEntries(config.requiredFlags.map((flag) => [flag, true])) }
|
|
66
|
+
const invalidInput = { ...validInput, [config.requiredFlags[0]]: false, idempotencyKey: 'invalid-key' }
|
|
67
|
+
const committed = []
|
|
68
|
+
const durable = new Map()
|
|
69
|
+
let active = null
|
|
70
|
+
const effects = {
|
|
71
|
+
async reserveIdempotency(key) { return durable.get(key) },
|
|
72
|
+
async transaction(work) {
|
|
73
|
+
const pending = []
|
|
74
|
+
active = pending
|
|
75
|
+
try {
|
|
76
|
+
const result = await work()
|
|
77
|
+
committed.push(...pending)
|
|
78
|
+
for (const entry of pending.filter((item) => item.kind === 'record')) durable.set(entry.value.idempotencyKey, entry.value.result)
|
|
79
|
+
return result
|
|
80
|
+
} finally { active = null }
|
|
81
|
+
},
|
|
82
|
+
async apply(input) {
|
|
83
|
+
if (input.failApply) throw new Error('injected mutation failure')
|
|
84
|
+
const result = { applied: input.idempotencyKey }
|
|
85
|
+
;(active ?? committed).push({ kind: 'apply', value: input })
|
|
86
|
+
return result
|
|
87
|
+
},
|
|
88
|
+
async record(value) { (active ?? committed).push({ kind: 'record', value }) },
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let invalidRejected = false
|
|
92
|
+
try { await action(invalidInput, effects) } catch { invalidRejected = true }
|
|
93
|
+
checks.push({ id: 'rejects-invalid-business-invariant', passed: invalidRejected && committed.length === 0 })
|
|
94
|
+
|
|
95
|
+
let failureRejected = false
|
|
96
|
+
try { await action({ ...validInput, failApply: true }, effects) } catch { failureRejected = true }
|
|
97
|
+
checks.push({ id: 'rolls-back-injected-mutation-failure', passed: failureRejected && committed.length === 0 && durable.size === 0 })
|
|
98
|
+
|
|
99
|
+
let result
|
|
100
|
+
let successError
|
|
101
|
+
try { result = await action(validInput, effects) } catch (error) { successError = error }
|
|
102
|
+
checks.push({ id: 'commits-mutation-and-lineage-together', passed: !successError && result?.applied === 'business-key' && committed.length === 2 && committed[0]?.kind === 'apply' && committed[1]?.kind === 'record' })
|
|
103
|
+
checks.push({ id: 'records-idempotency-lineage', passed: durable.get('business-key')?.applied === 'business-key' })
|
|
104
|
+
|
|
105
|
+
let duplicateError
|
|
106
|
+
try { await action(validInput, effects) } catch (error) { duplicateError = error }
|
|
107
|
+
checks.push({ id: 'deduplicates-retried-command', passed: !duplicateError && committed.length === 2 })
|
|
108
|
+
return { checks }
|
|
109
|
+
})();
|
|
110
|
+
`),
|
|
111
|
+
'ui-business-surface': new vm.Script(`
|
|
112
|
+
'use strict';
|
|
113
|
+
globalThis.__oracleResult = (async () => {
|
|
114
|
+
const config = __oracleConfig
|
|
115
|
+
const action = module.exports[config.exportName]
|
|
116
|
+
if (typeof action !== 'function') throw new Error('required UI business export is missing')
|
|
117
|
+
const checks = []
|
|
118
|
+
const validInput = { focusTarget: 'row-1', ...Object.fromEntries(config.requiredFlags.map((flag) => [flag, true])) }
|
|
119
|
+
const invalidInput = { ...validInput, [config.requiredFlags[0]]: false }
|
|
120
|
+
const events = []
|
|
121
|
+
const effects = {
|
|
122
|
+
async execute(input) { events.push('execute'); return { message: 'updated', input } },
|
|
123
|
+
async restoreFocus(target) { events.push('focus:' + target) },
|
|
124
|
+
async announce(message) { events.push('announce:' + message) },
|
|
125
|
+
}
|
|
126
|
+
let invalidRejected = false
|
|
127
|
+
try { await action(invalidInput, effects) } catch { invalidRejected = true }
|
|
128
|
+
checks.push({ id: 'rejects-invalid-ui-business-action', passed: invalidRejected && events.length === 0 })
|
|
129
|
+
let result
|
|
130
|
+
let successError
|
|
131
|
+
try { result = await action(validInput, effects) } catch (error) { successError = error }
|
|
132
|
+
checks.push({ id: 'executes-authorized-ui-action-once', passed: !successError && result?.message === 'updated' && events.filter((entry) => entry === 'execute').length === 1 })
|
|
133
|
+
checks.push({ id: 'restores-semantic-focus', passed: events.includes('focus:row-1') })
|
|
134
|
+
checks.push({ id: 'announces-observable-result', passed: events.includes('announce:updated') })
|
|
135
|
+
|
|
136
|
+
const failedEvents = []
|
|
137
|
+
let failureObserved = false
|
|
138
|
+
try {
|
|
139
|
+
await action(validInput, {
|
|
140
|
+
async execute() { failedEvents.push('execute'); throw new Error('injected UI failure') },
|
|
141
|
+
async restoreFocus(target) { failedEvents.push('focus:' + target) },
|
|
142
|
+
async announce(message) { failedEvents.push('announce:' + message) },
|
|
143
|
+
})
|
|
144
|
+
} catch { failureObserved = true }
|
|
145
|
+
checks.push({ id: 'restores-focus-after-ui-failure', passed: failureObserved && failedEvents.includes('focus:row-1') })
|
|
146
|
+
checks.push({ id: 'announces-ui-failure', passed: failedEvents.some((entry) => entry.startsWith('announce:')) })
|
|
147
|
+
return { checks }
|
|
148
|
+
})();
|
|
149
|
+
`),
|
|
150
|
+
'data-table-extension': new vm.Script(`
|
|
151
|
+
'use strict';
|
|
152
|
+
globalThis.__oracleResult = (async () => {
|
|
153
|
+
const config = __oracleConfig
|
|
154
|
+
const action = module.exports[config.exportName]
|
|
155
|
+
if (typeof action !== 'function') throw new Error('required DataTable bulk action export is missing')
|
|
156
|
+
const checks = []
|
|
157
|
+
const rows = [{ id: 'one', updatedAt: 'v1' }, { id: 'two', updatedAt: 'v2' }]
|
|
158
|
+
|
|
159
|
+
let deniedExecutions = 0
|
|
160
|
+
let denied = false
|
|
161
|
+
try {
|
|
162
|
+
await action(rows, {
|
|
163
|
+
async authorize() { return false },
|
|
164
|
+
async checkVersion() { return true },
|
|
165
|
+
async surfaceConflict() {},
|
|
166
|
+
async execute() { deniedExecutions += 1 },
|
|
167
|
+
})
|
|
168
|
+
} catch { denied = true }
|
|
169
|
+
checks.push({ id: 'denies-unauthorized-bulk-review', passed: denied && deniedExecutions === 0 })
|
|
170
|
+
|
|
171
|
+
const conflicts = []
|
|
172
|
+
let staleExecutions = 0
|
|
173
|
+
let stale = false
|
|
174
|
+
try {
|
|
175
|
+
await action(rows, {
|
|
176
|
+
async authorize() { return true },
|
|
177
|
+
async checkVersion(row) { return row.id !== 'two' },
|
|
178
|
+
async surfaceConflict(row) { conflicts.push(row.id) },
|
|
179
|
+
async execute() { staleExecutions += 1 },
|
|
180
|
+
})
|
|
181
|
+
} catch { stale = true }
|
|
182
|
+
checks.push({ id: 'checks-every-selected-version', passed: stale && conflicts.join(',') === 'two' && staleExecutions === 0 })
|
|
183
|
+
|
|
184
|
+
const checked = []
|
|
185
|
+
let executedRows = []
|
|
186
|
+
const result = await action(rows, {
|
|
187
|
+
async authorize(feature) { return feature === 'sales.orders.manage' },
|
|
188
|
+
async checkVersion(row) { checked.push(row.id + ':' + row.updatedAt); return true },
|
|
189
|
+
async surfaceConflict() {},
|
|
190
|
+
async execute(selectedRows) { executedRows = selectedRows; return { ok: true, progressJobId: 'job-1' } },
|
|
191
|
+
})
|
|
192
|
+
checks.push({ id: 'executes-exact-selected-orders', passed: checked.join(',') === 'one:v1,two:v2' && executedRows === rows })
|
|
193
|
+
checks.push({ id: 'returns-shared-progress-job', passed: result?.ok === true && result?.progressJobId === 'job-1' })
|
|
194
|
+
return { checks }
|
|
195
|
+
})();
|
|
196
|
+
`),
|
|
197
|
+
'async-operation': new vm.Script(`
|
|
198
|
+
'use strict';
|
|
199
|
+
globalThis.__oracleResult = (async () => {
|
|
200
|
+
const config = __oracleConfig
|
|
201
|
+
const action = module.exports[config.exportName]
|
|
202
|
+
if (typeof action !== 'function') throw new Error('required async operation export is missing')
|
|
203
|
+
const checks = []
|
|
204
|
+
const items = [{ id: 'one' }, { id: 'paid-or-skipped', skip: true }, { id: 'three' }]
|
|
205
|
+
const applied = []
|
|
206
|
+
const progress = []
|
|
207
|
+
const undo = []
|
|
208
|
+
const effects = {
|
|
209
|
+
async isCancelled() { return false },
|
|
210
|
+
async shouldSkip(item) { return item.skip === true },
|
|
211
|
+
async applyChunk(chunk) { applied.push(...chunk.map((item) => item.id)); return { ids: chunk.map((item) => item.id) } },
|
|
212
|
+
async reportProgress(value) { progress.push(value) },
|
|
213
|
+
async registerUndo(value) { undo.push(value) },
|
|
214
|
+
}
|
|
215
|
+
let successError
|
|
216
|
+
try { await action(items, effects) } catch (error) { successError = error }
|
|
217
|
+
checks.push({ id: 'skips-ineligible-or-paid-work', passed: !successError && applied.join(',') === 'one,three' })
|
|
218
|
+
checks.push({ id: 'reports-monotonic-progress', passed: progress.length === 2 && progress[0]?.completed < progress[1]?.completed && progress[1]?.total === 3 })
|
|
219
|
+
checks.push({ id: 'registers-compensating-undo', passed: undo.length === 2 && undo.every((entry) => Array.isArray(entry?.ids)) })
|
|
220
|
+
|
|
221
|
+
const cancelledApplied = []
|
|
222
|
+
let cancellationChecks = 0
|
|
223
|
+
await action(items, {
|
|
224
|
+
async isCancelled() { cancellationChecks += 1; return cancelledApplied.length >= 1 },
|
|
225
|
+
async shouldSkip(item) { return item.skip === true },
|
|
226
|
+
async applyChunk(chunk) { cancelledApplied.push(...chunk.map((item) => item.id)); return { ids: chunk.map((item) => item.id) } },
|
|
227
|
+
async reportProgress() {},
|
|
228
|
+
async registerUndo() {},
|
|
229
|
+
})
|
|
230
|
+
checks.push({ id: 'stops-at-cancellation-boundary', passed: cancellationChecks >= 2 && cancelledApplied.join(',') === 'one' })
|
|
231
|
+
return { checks }
|
|
232
|
+
})();
|
|
233
|
+
`),
|
|
234
|
+
'ai-safe-agent': new vm.Script(`
|
|
235
|
+
'use strict';
|
|
236
|
+
globalThis.__oracleResult = (async () => {
|
|
237
|
+
const config = __oracleConfig
|
|
238
|
+
const action = module.exports[config.exportName]
|
|
239
|
+
if (typeof action !== 'function') throw new Error('required AI action export is missing')
|
|
240
|
+
const checks = []
|
|
241
|
+
if (config.mode === 'mutation') {
|
|
242
|
+
const events = []
|
|
243
|
+
let denied = false
|
|
244
|
+
try { await action({ change: 'quote' }, { async authorize() { return false }, async prepareMutation() { events.push('prepare') }, async execute() { events.push('execute') } }) } catch { denied = true }
|
|
245
|
+
checks.push({ id: 'denies-unauthorized-ai-mutation', passed: denied && events.length === 0 })
|
|
246
|
+
const pending = await action({ change: 'quote' }, { async authorize() { return true }, async prepareMutation() { events.push('prepare'); return { approved: false } }, async execute() { events.push('execute') } })
|
|
247
|
+
checks.push({ id: 'does-not-execute-pending-mutation', passed: pending?.status === 'pending' && !events.includes('execute') })
|
|
248
|
+
let approvedError
|
|
249
|
+
try { await action({ change: 'quote' }, { async authorize() { events.push('authorize'); return true }, async prepareMutation(input) { events.push('prepare-approved'); return { approved: true, input } }, async execute() { events.push('execute'); return { saved: true } } }) } catch (error) { approvedError = error }
|
|
250
|
+
checks.push({ id: 'executes-only-after-approval', passed: !approvedError && events.slice(-3).join(',') === 'authorize,prepare-approved,execute' })
|
|
251
|
+
} else {
|
|
252
|
+
const delegated = []
|
|
253
|
+
let denied = false
|
|
254
|
+
try { await action({ question: 'stock' }, { async authorize() { return false }, async delegate(...args) { delegated.push(args) } }) } catch { denied = true }
|
|
255
|
+
checks.push({ id: 'denies-unauthorized-delegation', passed: denied && delegated.length === 0 })
|
|
256
|
+
await action({ question: 'stock', requestedMutation: true, allowedTools: ['catalog.read'] }, { async authorize() { return true }, async delegate(input, options) { delegated.push([input, options]); return { answer: 'ok' } } })
|
|
257
|
+
checks.push({ id: 'caps-delegated-authority', passed: delegated.length === 1 && delegated[0][1]?.authority === 'read-only' })
|
|
258
|
+
checks.push({ id: 'passes-explicit-tool-allowlist', passed: delegated[0][1]?.allowedTools?.join(',') === 'catalog.read' })
|
|
259
|
+
}
|
|
260
|
+
return { checks }
|
|
261
|
+
})();
|
|
262
|
+
`),
|
|
263
|
+
'provider-adapter': new vm.Script(`
|
|
264
|
+
'use strict';
|
|
265
|
+
globalThis.__oracleResult = (async () => {
|
|
266
|
+
const config = __oracleConfig
|
|
267
|
+
const action = module.exports[config.exportName]
|
|
268
|
+
if (typeof action !== 'function') throw new Error('required provider adapter export is missing')
|
|
269
|
+
const checks = []
|
|
270
|
+
let requests = 0
|
|
271
|
+
const duplicate = await action({ idempotencyKey: 'existing', maxAttempts: 2 }, {
|
|
272
|
+
async findExisting() { return { id: 'prior' } },
|
|
273
|
+
async request() { requests += 1 },
|
|
274
|
+
async reconcile(value) { return value },
|
|
275
|
+
redact(value) { return value },
|
|
276
|
+
})
|
|
277
|
+
checks.push({ id: 'returns-existing-idempotent-result', passed: duplicate?.id === 'prior' && requests === 0 })
|
|
278
|
+
|
|
279
|
+
let reconciled = 0
|
|
280
|
+
let retryError
|
|
281
|
+
let result
|
|
282
|
+
try {
|
|
283
|
+
result = await action({ idempotencyKey: 'new', maxAttempts: 2 }, {
|
|
284
|
+
async findExisting() { return undefined },
|
|
285
|
+
async request() { requests += 1; if (requests === 1) throw new Error('transient'); return { id: 'provider-id', secret: 'hidden' } },
|
|
286
|
+
async reconcile(value) { reconciled += 1; return value },
|
|
287
|
+
redact(value) { return { id: value.id } },
|
|
288
|
+
})
|
|
289
|
+
} catch (error) { retryError = error }
|
|
290
|
+
checks.push({ id: 'retries-transient-provider-failure', passed: !retryError && requests === 2 })
|
|
291
|
+
checks.push({ id: 'reconciles-and-redacts-result', passed: reconciled === 1 && result?.id === 'provider-id' && !Object.prototype.hasOwnProperty.call(result ?? {}, 'secret') })
|
|
292
|
+
|
|
293
|
+
let terminalMessage = ''
|
|
294
|
+
try {
|
|
295
|
+
await action({ idempotencyKey: 'terminal', maxAttempts: 2 }, {
|
|
296
|
+
async findExisting() { return undefined },
|
|
297
|
+
async request() { throw new Error('secret-token-value') },
|
|
298
|
+
async reconcile(value) { return value },
|
|
299
|
+
redact(value) { return value },
|
|
300
|
+
})
|
|
301
|
+
} catch (error) { terminalMessage = String(error?.message ?? error) }
|
|
302
|
+
checks.push({ id: 'redacts-terminal-provider-error', passed: terminalMessage.length > 0 && !terminalMessage.includes('secret-token-value') })
|
|
303
|
+
return { checks }
|
|
304
|
+
})();
|
|
305
|
+
`),
|
|
306
|
+
'endpoint-guard': new vm.Script(`
|
|
307
|
+
'use strict';
|
|
308
|
+
globalThis.__oracleResult = (async () => {
|
|
309
|
+
const guard = module.exports.assertSafeEndpoint
|
|
310
|
+
if (typeof guard !== 'function') throw new Error('required assertSafeEndpoint export is missing')
|
|
311
|
+
const checks = []
|
|
312
|
+
const unsafe = [
|
|
313
|
+
'http://calendar.example/events',
|
|
314
|
+
'https://localhost/events',
|
|
315
|
+
'https://127.0.0.1/events',
|
|
316
|
+
'https://[::1]/events',
|
|
317
|
+
'https://10.1.2.3/events',
|
|
318
|
+
'https://172.16.0.1/events',
|
|
319
|
+
'https://172.31.255.254/events',
|
|
320
|
+
'https://192.168.1.1/events',
|
|
321
|
+
'https://169.254.169.254/latest/meta-data',
|
|
322
|
+
'https://metadata.internal/latest/meta-data',
|
|
323
|
+
]
|
|
324
|
+
const rejected = []
|
|
325
|
+
for (const endpoint of unsafe) {
|
|
326
|
+
try { await guard(endpoint) } catch { rejected.push(endpoint) }
|
|
327
|
+
}
|
|
328
|
+
checks.push({ id: 'rejects-non-https-and-private-endpoints', passed: rejected.length === unsafe.length })
|
|
329
|
+
let publicError
|
|
330
|
+
try { await guard('https://calendar.example/events') } catch (error) { publicError = error }
|
|
331
|
+
checks.push({ id: 'accepts-public-https-endpoint', passed: !publicError })
|
|
332
|
+
return { checks }
|
|
333
|
+
})();
|
|
334
|
+
`),
|
|
335
|
+
'data-flow': new vm.Script(`
|
|
336
|
+
'use strict';
|
|
337
|
+
globalThis.__oracleResult = (async () => {
|
|
338
|
+
const config = __oracleConfig
|
|
339
|
+
const action = module.exports[config.exportName]
|
|
340
|
+
if (typeof action !== 'function') throw new Error('required data-flow export is missing')
|
|
341
|
+
const checks = []
|
|
342
|
+
const events = []
|
|
343
|
+
const applied = []
|
|
344
|
+
let result
|
|
345
|
+
let flowError
|
|
346
|
+
try {
|
|
347
|
+
result = await action({ cursor: 'current' }, {
|
|
348
|
+
async fetchPage() { events.push('fetch'); return { items: [{ id: 'one', value: '=1+1' }, { id: 'bad', value: 'bad' }, { id: 'two', value: 'ok' }], nextCursor: 'next' } },
|
|
349
|
+
sanitize(row) { events.push('sanitize:' + row.id); return { ...row, value: String(row.value).startsWith('=') ? "'" + row.value : row.value } },
|
|
350
|
+
async apply(row) { events.push('apply:' + row.id); if (row.id === 'bad') throw new Error('row rejected'); applied.push(row) },
|
|
351
|
+
async commitCursor(cursor) { events.push('commit:' + cursor) },
|
|
352
|
+
})
|
|
353
|
+
} catch (error) { flowError = error }
|
|
354
|
+
checks.push({ id: 'isolates-bad-row', passed: !flowError && result?.errors?.length === 1 && applied.map((row) => row.id).join(',') === 'one,two' })
|
|
355
|
+
checks.push({ id: 'neutralizes-formula-content', passed: applied[0]?.value === "'=1+1" })
|
|
356
|
+
checks.push({ id: 'commits-cursor-after-rows', passed: events.at(-1) === 'commit:next' })
|
|
357
|
+
|
|
358
|
+
let failedCommit = false
|
|
359
|
+
let fetchRejected = false
|
|
360
|
+
try {
|
|
361
|
+
await action({ cursor: 'stable' }, {
|
|
362
|
+
async fetchPage() { throw new Error('page failed') },
|
|
363
|
+
sanitize(row) { return row },
|
|
364
|
+
async apply() {},
|
|
365
|
+
async commitCursor() { failedCommit = true },
|
|
366
|
+
})
|
|
367
|
+
} catch { fetchRejected = true }
|
|
368
|
+
checks.push({ id: 'preserves-cursor-on-page-failure', passed: fetchRejected && !failedCommit })
|
|
369
|
+
return { checks }
|
|
370
|
+
})();
|
|
371
|
+
`),
|
|
372
|
+
'crm-library-regression': new vm.Script(`
|
|
373
|
+
'use strict';
|
|
374
|
+
globalThis.__oracleResult = (async () => {
|
|
375
|
+
const required = ['createBook', 'undoCreateBook', 'deleteBook', 'undoDeleteBook', 'checkoutBook', 'returnLoan', 'renewLoan', 'markLoanLost']
|
|
376
|
+
const actions = Object.fromEntries(required.map((name) => [name, module.exports[name]]))
|
|
377
|
+
if (required.some((name) => typeof actions[name] !== 'function')) throw new Error('required CRM library export is missing')
|
|
378
|
+
|
|
379
|
+
const checks = []
|
|
380
|
+
const trustedScope = { tenantId: 'tenant-a', organizationId: 'org-a' }
|
|
381
|
+
const attackerScope = { tenantId: 'tenant-b', organizationId: 'org-b' }
|
|
382
|
+
const books = new Map()
|
|
383
|
+
const loans = new Map()
|
|
384
|
+
const idempotency = new Map()
|
|
385
|
+
const activeByBook = new Map()
|
|
386
|
+
const scopeEvents = []
|
|
387
|
+
const customerEvents = []
|
|
388
|
+
let effectCalls = 0
|
|
389
|
+
|
|
390
|
+
const assertScope = (scope) => {
|
|
391
|
+
effectCalls += 1
|
|
392
|
+
scopeEvents.push(scope?.tenantId + ':' + scope?.organizationId)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const effects = {
|
|
396
|
+
async createBook(record, scope) {
|
|
397
|
+
assertScope(scope)
|
|
398
|
+
const stored = { ...record, tenantId: scope.tenantId, organizationId: scope.organizationId, deletedAt: null }
|
|
399
|
+
books.set(stored.id, stored)
|
|
400
|
+
return { ...stored }
|
|
401
|
+
},
|
|
402
|
+
async softDeleteBook(id, scope) {
|
|
403
|
+
assertScope(scope)
|
|
404
|
+
const book = books.get(id)
|
|
405
|
+
if (!book) throw new Error('book not found')
|
|
406
|
+
book.deletedAt = 'deleted'
|
|
407
|
+
return { ...book }
|
|
408
|
+
},
|
|
409
|
+
async restoreBook(id, scope) {
|
|
410
|
+
assertScope(scope)
|
|
411
|
+
const book = books.get(id)
|
|
412
|
+
if (!book) throw new Error('book not found')
|
|
413
|
+
book.deletedAt = null
|
|
414
|
+
return { ...book }
|
|
415
|
+
},
|
|
416
|
+
async resolveCustomer(id, scope) {
|
|
417
|
+
assertScope(scope)
|
|
418
|
+
customerEvents.push({ id, scope: { ...scope } })
|
|
419
|
+
return { id, displayName: 'Scoped CRM customer' }
|
|
420
|
+
},
|
|
421
|
+
async findActiveLoan(bookId, scope) {
|
|
422
|
+
assertScope(scope)
|
|
423
|
+
await Promise.resolve()
|
|
424
|
+
const loanId = activeByBook.get(bookId)
|
|
425
|
+
return loanId ? { ...loans.get(loanId) } : undefined
|
|
426
|
+
},
|
|
427
|
+
async createLoan(input, scope) {
|
|
428
|
+
assertScope(scope)
|
|
429
|
+
await Promise.resolve()
|
|
430
|
+
const loan = { ...input, id: 'loan-' + (loans.size + 1), status: 'active' }
|
|
431
|
+
loans.set(loan.id, loan)
|
|
432
|
+
activeByBook.set(loan.bookId, loan.id)
|
|
433
|
+
return { ...loan }
|
|
434
|
+
},
|
|
435
|
+
async claimCheckout(input, scope) {
|
|
436
|
+
assertScope(scope)
|
|
437
|
+
if ('memberId' in input) throw new Error('duplicate local member leaked into checkout')
|
|
438
|
+
const priorId = idempotency.get(input.idempotencyKey)
|
|
439
|
+
if (priorId) return { status: 'existing', loan: { ...loans.get(priorId) } }
|
|
440
|
+
if (activeByBook.has(input.bookId)) return { status: 'conflict' }
|
|
441
|
+
const loan = { ...input, id: 'loan-' + (loans.size + 1), tenantId: scope.tenantId, organizationId: scope.organizationId, status: 'active' }
|
|
442
|
+
loans.set(loan.id, loan)
|
|
443
|
+
activeByBook.set(input.bookId, loan.id)
|
|
444
|
+
idempotency.set(input.idempotencyKey, loan.id)
|
|
445
|
+
return { status: 'created', loan: { ...loan } }
|
|
446
|
+
},
|
|
447
|
+
async findLoan(id, scope) {
|
|
448
|
+
assertScope(scope)
|
|
449
|
+
const loan = loans.get(id)
|
|
450
|
+
return loan ? { ...loan } : undefined
|
|
451
|
+
},
|
|
452
|
+
async updateLoan(id, scope, patch) {
|
|
453
|
+
assertScope(scope)
|
|
454
|
+
const loan = loans.get(id)
|
|
455
|
+
if (!loan) throw new Error('loan not found')
|
|
456
|
+
Object.assign(loan, patch)
|
|
457
|
+
return { ...loan }
|
|
458
|
+
},
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
let missingScopeRejected = false
|
|
462
|
+
const callsBeforeMissingScope = effectCalls
|
|
463
|
+
try {
|
|
464
|
+
await actions.checkoutBook({ bookId: 'missing-scope', customerEntityId: 'customer-a', idempotencyKey: 'missing-scope' }, {}, effects)
|
|
465
|
+
} catch { missingScopeRejected = true }
|
|
466
|
+
checks.push({ id: 'fails-closed-before-effects', passed: missingScopeRejected && effectCalls === callsBeforeMissingScope })
|
|
467
|
+
|
|
468
|
+
await actions.createBook({ id: 'created-book', title: 'Created', ...attackerScope }, trustedScope, effects)
|
|
469
|
+
await actions.undoCreateBook('created-book', trustedScope, effects)
|
|
470
|
+
checks.push({ id: 'create-undo-soft-deletes', passed: books.get('created-book')?.deletedAt === 'deleted' })
|
|
471
|
+
|
|
472
|
+
await actions.createBook({ id: 'restored-book', title: 'Restored', ...attackerScope }, trustedScope, effects)
|
|
473
|
+
await actions.deleteBook('restored-book', trustedScope, effects)
|
|
474
|
+
await actions.undoDeleteBook('restored-book', trustedScope, effects)
|
|
475
|
+
checks.push({ id: 'delete-undo-restores', passed: books.get('restored-book')?.deletedAt === null })
|
|
476
|
+
checks.push({ id: 'body-scope-cannot-override-trusted-scope', passed: [...books.values()].every((book) => book.tenantId === trustedScope.tenantId && book.organizationId === trustedScope.organizationId) })
|
|
477
|
+
|
|
478
|
+
const checkoutInput = { bookId: 'shared-copy', customerEntityId: 'customer-a', ...attackerScope }
|
|
479
|
+
const concurrent = await Promise.all([
|
|
480
|
+
actions.checkoutBook({ ...checkoutInput, idempotencyKey: 'checkout-a' }, trustedScope, effects).catch(() => ({ status: 'conflict' })),
|
|
481
|
+
actions.checkoutBook({ ...checkoutInput, idempotencyKey: 'checkout-b' }, trustedScope, effects).catch(() => ({ status: 'conflict' })),
|
|
482
|
+
])
|
|
483
|
+
const statuses = concurrent.map((result) => result?.status).sort()
|
|
484
|
+
checks.push({ id: 'concurrent-checkout-has-one-winner', passed: statuses.join(',') === 'conflict,created' && [...loans.values()].filter((loan) => loan.bookId === 'shared-copy' && loan.status === 'active').length === 1 })
|
|
485
|
+
|
|
486
|
+
const winner = concurrent.find((result) => result?.status === 'created')
|
|
487
|
+
const winnerKey = winner?.loan?.id === loans.get(idempotency.get('checkout-a'))?.id ? 'checkout-a' : 'checkout-b'
|
|
488
|
+
const retry = await actions.checkoutBook({ ...checkoutInput, idempotencyKey: winnerKey }, trustedScope, effects)
|
|
489
|
+
checks.push({ id: 'checkout-retry-is-deterministic', passed: retry?.status === 'existing' && retry?.loan?.id === winner?.loan?.id && loans.size === 1 })
|
|
490
|
+
checks.push({ id: 'resolves-scoped-crm-customer-snapshot', passed: customerEvents.length >= 2 && customerEvents.every((event) => event.id === 'customer-a' && event.scope.tenantId === trustedScope.tenantId) && winner?.loan?.customerEntityId === 'customer-a' && winner?.loan?.customerNameSnapshot === 'Scoped CRM customer' && !('memberId' in (winner?.loan ?? {})) })
|
|
491
|
+
|
|
492
|
+
for (const [id, action] of [['return-loan', actions.returnLoan], ['renew-loan', actions.renewLoan], ['lost-loan', actions.markLoanLost]]) {
|
|
493
|
+
loans.set(id, { id, bookId: id, customerEntityId: 'customer-a', status: 'active' })
|
|
494
|
+
await action({ id, ...attackerScope }, trustedScope, effects)
|
|
495
|
+
}
|
|
496
|
+
checks.push({ id: 'all-loan-actions-use-trusted-scope', passed: ['return-loan', 'renew-loan', 'lost-loan'].every((id) => loans.get(id)?.status !== 'active') && scopeEvents.every((entry) => entry === 'tenant-a:org-a') })
|
|
497
|
+
return { checks }
|
|
498
|
+
})();
|
|
499
|
+
`),
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const PROBES = {
|
|
503
|
+
'OMH-045': new vm.Script(`
|
|
504
|
+
'use strict';
|
|
505
|
+
globalThis.__oracleResult = (async () => {
|
|
506
|
+
const fetchPage = module.exports.fetchPage
|
|
507
|
+
if (typeof fetchPage !== 'function') throw new Error('required export fetchPage is missing')
|
|
508
|
+
const checks = []
|
|
509
|
+
const state = { cursor: 'cursor-current' }
|
|
510
|
+
const urls = []
|
|
511
|
+
let attempt = 0
|
|
512
|
+
let page
|
|
513
|
+
let retryError
|
|
514
|
+
const effects = {
|
|
515
|
+
maxAttempts: 2,
|
|
516
|
+
async fetch(url) {
|
|
517
|
+
urls.push(url)
|
|
518
|
+
attempt += 1
|
|
519
|
+
if (attempt === 1) return { ok: false, status: 503, async json() { return {} } }
|
|
520
|
+
return { ok: true, status: 200, async json() { return { items: ['record'], nextCursor: 'cursor-next' } } }
|
|
521
|
+
},
|
|
522
|
+
}
|
|
523
|
+
try { page = await fetchPage('https://provider.test/records', state, effects) } catch (error) { retryError = error }
|
|
524
|
+
checks.push({ id: 'retries-transient-response', passed: !retryError && attempt === 2 })
|
|
525
|
+
checks.push({ id: 'retries-same-cursor', passed: urls.length === 2 && urls.every((url) => url.endsWith('cursor=cursor-current')) })
|
|
526
|
+
checks.push({ id: 'commits-cursor-after-success', passed: state.cursor === 'cursor-next' && page?.nextCursor === 'cursor-next' })
|
|
527
|
+
|
|
528
|
+
const failedState = { cursor: 'cursor-stable' }
|
|
529
|
+
let failedAttempts = 0
|
|
530
|
+
let terminalRejected = false
|
|
531
|
+
try {
|
|
532
|
+
await fetchPage('https://provider.test/records', failedState, {
|
|
533
|
+
maxAttempts: 2,
|
|
534
|
+
async fetch() {
|
|
535
|
+
failedAttempts += 1
|
|
536
|
+
return { ok: false, status: 503, async json() { return {} } }
|
|
537
|
+
},
|
|
538
|
+
})
|
|
539
|
+
} catch { terminalRejected = true }
|
|
540
|
+
checks.push({ id: 'bounds-retries', passed: terminalRejected && failedAttempts === 2 })
|
|
541
|
+
checks.push({ id: 'preserves-cursor-after-terminal-failure', passed: failedState.cursor === 'cursor-stable' })
|
|
542
|
+
|
|
543
|
+
let privateFetchCalls = 0
|
|
544
|
+
let privateEndpointRejected = false
|
|
545
|
+
try {
|
|
546
|
+
await fetchPage('http://127.0.0.1/admin', { cursor: 'cursor-private' }, {
|
|
547
|
+
maxAttempts: 2,
|
|
548
|
+
async fetch() {
|
|
549
|
+
privateFetchCalls += 1
|
|
550
|
+
return { ok: true, status: 200, async json() { return { items: [] } } }
|
|
551
|
+
},
|
|
552
|
+
})
|
|
553
|
+
} catch { privateEndpointRejected = true }
|
|
554
|
+
checks.push({ id: 'rejects-private-endpoint-before-fetch', passed: privateEndpointRejected && privateFetchCalls === 0 })
|
|
555
|
+
return { checks }
|
|
556
|
+
})();
|
|
557
|
+
`),
|
|
558
|
+
'OMH-054': new vm.Script(`
|
|
559
|
+
'use strict';
|
|
560
|
+
globalThis.__oracleResult = (async () => {
|
|
561
|
+
const callApiActivity = module.exports.callApiActivity
|
|
562
|
+
if (typeof callApiActivity !== 'function') throw new Error('required export callApiActivity is missing')
|
|
563
|
+
const checks = []
|
|
564
|
+
const durable = new Set()
|
|
565
|
+
const posts = []
|
|
566
|
+
const events = []
|
|
567
|
+
let transactionState = null
|
|
568
|
+
const effects = {
|
|
569
|
+
async reserveIdempotency(key) {
|
|
570
|
+
events.push('reserve:' + key)
|
|
571
|
+
if (transactionState) transactionState.add(key)
|
|
572
|
+
else durable.add(key)
|
|
573
|
+
return key
|
|
574
|
+
},
|
|
575
|
+
async transaction(work) {
|
|
576
|
+
events.push('transaction:start')
|
|
577
|
+
const staged = new Set()
|
|
578
|
+
transactionState = staged
|
|
579
|
+
try {
|
|
580
|
+
const result = await work()
|
|
581
|
+
for (const key of staged) durable.add(key)
|
|
582
|
+
events.push('transaction:commit')
|
|
583
|
+
return result
|
|
584
|
+
} catch (error) {
|
|
585
|
+
events.push('transaction:rollback')
|
|
586
|
+
throw error
|
|
587
|
+
} finally {
|
|
588
|
+
transactionState = null
|
|
589
|
+
}
|
|
590
|
+
},
|
|
591
|
+
async post(url, options) {
|
|
592
|
+
posts.push({ url, key: options?.idempotencyKey })
|
|
593
|
+
if (posts.length === 1) throw new Error('injected post failure')
|
|
594
|
+
return { ok: true }
|
|
595
|
+
},
|
|
596
|
+
}
|
|
597
|
+
const input = { url: 'https://provider.test/action', idempotencyKey: 'activity-key' }
|
|
598
|
+
let firstRejected = false
|
|
599
|
+
try { await callApiActivity(input, effects) } catch { firstRejected = true }
|
|
600
|
+
checks.push({ id: 'injected-failure-rolls-back-transaction', passed: firstRejected && events.includes('transaction:rollback') })
|
|
601
|
+
checks.push({ id: 'idempotency-survives-rollback', passed: durable.has('activity-key') })
|
|
602
|
+
let retryError
|
|
603
|
+
try { await callApiActivity(input, effects) } catch (error) { retryError = error }
|
|
604
|
+
checks.push({ id: 'retry-reuses-idempotency-key', passed: !retryError && posts.length === 2 && posts.every((post) => post.key === 'activity-key') })
|
|
605
|
+
checks.push({ id: 'reservation-precedes-transaction', passed: events[0] === 'reserve:activity-key' && events[1] === 'transaction:start' })
|
|
606
|
+
return { checks }
|
|
607
|
+
})();
|
|
608
|
+
`),
|
|
609
|
+
'OMH-057': new vm.Script(`
|
|
610
|
+
'use strict';
|
|
611
|
+
globalThis.__oracleResult = (async () => {
|
|
612
|
+
const listRecords = module.exports.listRecords
|
|
613
|
+
if (typeof listRecords !== 'function') throw new Error('required export listRecords is missing')
|
|
614
|
+
const checks = []
|
|
615
|
+
const calls = []
|
|
616
|
+
const store = { async find(filter) { calls.push(filter); return ['record'] } }
|
|
617
|
+
let missingTenantRejected = false
|
|
618
|
+
try { await listRecords({ organizationId: 'organization-1' }, store) } catch { missingTenantRejected = true }
|
|
619
|
+
let missingOrganizationRejected = false
|
|
620
|
+
try { await listRecords({ tenantId: 'tenant-1' }, store) } catch { missingOrganizationRejected = true }
|
|
621
|
+
checks.push({ id: 'rejects-missing-tenant', passed: missingTenantRejected })
|
|
622
|
+
checks.push({ id: 'rejects-missing-organization', passed: missingOrganizationRejected })
|
|
623
|
+
checks.push({ id: 'does-not-query-with-missing-scope', passed: calls.length === 0 })
|
|
624
|
+
let validResult
|
|
625
|
+
let validError
|
|
626
|
+
try { validResult = await listRecords({ tenantId: 'tenant-1', organizationId: 'organization-1' }, store) } catch (error) { validError = error }
|
|
627
|
+
const filter = calls[0]
|
|
628
|
+
checks.push({
|
|
629
|
+
id: 'queries-with-complete-scope',
|
|
630
|
+
passed: !validError && Array.isArray(validResult) && calls.length === 1
|
|
631
|
+
&& filter?.tenant_id === 'tenant-1' && filter?.organization_id === 'organization-1',
|
|
632
|
+
})
|
|
633
|
+
return { checks }
|
|
634
|
+
})();
|
|
635
|
+
`),
|
|
636
|
+
'OMH-060': new vm.Script(`
|
|
637
|
+
'use strict';
|
|
638
|
+
globalThis.__oracleResult = (async () => {
|
|
639
|
+
const updateRecord = module.exports.updateRecord
|
|
640
|
+
if (typeof updateRecord !== 'function') throw new Error('required export updateRecord is missing')
|
|
641
|
+
const checks = []
|
|
642
|
+
function makeStore() {
|
|
643
|
+
const committed = []
|
|
644
|
+
const store = {
|
|
645
|
+
committed,
|
|
646
|
+
async persist(value) { committed.push({ ...value }) },
|
|
647
|
+
async flush() {},
|
|
648
|
+
async transaction(work) {
|
|
649
|
+
const pending = []
|
|
650
|
+
const transactionalStore = {
|
|
651
|
+
async persist(value) { pending.push({ ...value }) },
|
|
652
|
+
async flush() {},
|
|
653
|
+
}
|
|
654
|
+
const result = await work(transactionalStore)
|
|
655
|
+
committed.push(...pending)
|
|
656
|
+
return result
|
|
657
|
+
},
|
|
658
|
+
}
|
|
659
|
+
return store
|
|
660
|
+
}
|
|
661
|
+
const failedStore = makeStore()
|
|
662
|
+
let injectedFailureObserved = false
|
|
663
|
+
try { await updateRecord(failedStore, true) } catch { injectedFailureObserved = true }
|
|
664
|
+
checks.push({ id: 'propagates-injected-failure', passed: injectedFailureObserved })
|
|
665
|
+
checks.push({ id: 'rolls-back-all-phases', passed: failedStore.committed.length === 0 })
|
|
666
|
+
const successStore = makeStore()
|
|
667
|
+
let successError
|
|
668
|
+
try { await updateRecord(successStore, false) } catch (error) { successError = error }
|
|
669
|
+
checks.push({
|
|
670
|
+
id: 'commits-complete-write-on-success',
|
|
671
|
+
passed: !successError && successStore.committed.length === 2
|
|
672
|
+
&& successStore.committed[0]?.phase === 1 && successStore.committed[1]?.phase === 2,
|
|
673
|
+
})
|
|
674
|
+
return { checks }
|
|
675
|
+
})();
|
|
676
|
+
`),
|
|
677
|
+
'OMH-061': new vm.Script(`
|
|
678
|
+
'use strict';
|
|
679
|
+
globalThis.__oracleResult = (async () => {
|
|
680
|
+
const toInitialValues = module.exports.toInitialValues
|
|
681
|
+
const toUpdatePayload = module.exports.toUpdatePayload
|
|
682
|
+
if (typeof toInitialValues !== 'function' || typeof toUpdatePayload !== 'function') {
|
|
683
|
+
throw new Error('required nullable round-trip exports are missing')
|
|
684
|
+
}
|
|
685
|
+
const checks = []
|
|
686
|
+
const initialNull = toInitialValues({ note: null })
|
|
687
|
+
checks.push({ id: 'loads-explicit-null', passed: initialNull?.note === null })
|
|
688
|
+
const cleared = toUpdatePayload({ note: '' })
|
|
689
|
+
checks.push({ id: 'serializes-clear-as-null', passed: Object.prototype.hasOwnProperty.call(cleared ?? {}, 'note') && cleared.note === null })
|
|
690
|
+
const reloaded = toInitialValues(cleared)
|
|
691
|
+
checks.push({ id: 'reload-preserves-null', passed: reloaded?.note === null })
|
|
692
|
+
const text = 'kept value'
|
|
693
|
+
const nonNull = toUpdatePayload(toInitialValues({ note: text }))
|
|
694
|
+
checks.push({ id: 'round-trips-non-null-value', passed: nonNull?.note === text })
|
|
695
|
+
return { checks }
|
|
696
|
+
})();
|
|
697
|
+
`),
|
|
698
|
+
'OMH-070': new vm.Script(`
|
|
699
|
+
'use strict';
|
|
700
|
+
globalThis.__oracleResult = (async () => {
|
|
701
|
+
const syncPage = module.exports.syncPage
|
|
702
|
+
if (typeof syncPage !== 'function') throw new Error('required export syncPage is missing')
|
|
703
|
+
const checks = []
|
|
704
|
+
const state = { cursor: 'cursor-current' }
|
|
705
|
+
const received = []
|
|
706
|
+
let attempt = 0
|
|
707
|
+
let page
|
|
708
|
+
let retryError
|
|
709
|
+
const provider = {
|
|
710
|
+
maxAttempts: 2,
|
|
711
|
+
nextCursor: 'cursor-next',
|
|
712
|
+
async fetchPage(cursor) {
|
|
713
|
+
received.push(cursor)
|
|
714
|
+
attempt += 1
|
|
715
|
+
if (attempt === 1) throw new Error('transient provider failure')
|
|
716
|
+
return { items: ['record'], nextCursor: 'cursor-next' }
|
|
717
|
+
},
|
|
718
|
+
}
|
|
719
|
+
try { page = await syncPage(state, provider) } catch (error) { retryError = error }
|
|
720
|
+
checks.push({ id: 'retries-transient-page', passed: !retryError && attempt === 2 })
|
|
721
|
+
checks.push({ id: 'retries-current-cursor', passed: received.length === 2 && received.every((cursor) => cursor === 'cursor-current') })
|
|
722
|
+
checks.push({ id: 'advances-after-success', passed: state.cursor === 'cursor-next' && page?.nextCursor === 'cursor-next' })
|
|
723
|
+
|
|
724
|
+
const failedState = { cursor: 'cursor-stable' }
|
|
725
|
+
let failedAttempts = 0
|
|
726
|
+
let terminalRejected = false
|
|
727
|
+
try {
|
|
728
|
+
await syncPage(failedState, {
|
|
729
|
+
maxAttempts: 2,
|
|
730
|
+
nextCursor: 'cursor-never',
|
|
731
|
+
async fetchPage(cursor) {
|
|
732
|
+
if (cursor !== 'cursor-stable') throw new Error('cursor advanced before success')
|
|
733
|
+
failedAttempts += 1
|
|
734
|
+
throw new Error('transient provider failure')
|
|
735
|
+
},
|
|
736
|
+
})
|
|
737
|
+
} catch { terminalRejected = true }
|
|
738
|
+
checks.push({ id: 'bounds-page-retries', passed: terminalRejected && failedAttempts === 2 })
|
|
739
|
+
checks.push({ id: 'preserves-cursor-after-terminal-failure', passed: failedState.cursor === 'cursor-stable' })
|
|
740
|
+
return { checks }
|
|
741
|
+
})();
|
|
742
|
+
`),
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const ORACLE_CONFIG_SCRIPT = new vm.Script(`
|
|
746
|
+
'use strict';
|
|
747
|
+
globalThis.__oracleConfig = Object.freeze({
|
|
748
|
+
exportName: globalThis.__oracleExportName,
|
|
749
|
+
mode: globalThis.__oracleMode,
|
|
750
|
+
requiredFlags: Object.freeze(JSON.parse(globalThis.__oracleRequiredFlagsJson)),
|
|
751
|
+
});
|
|
752
|
+
`)
|
|
753
|
+
|
|
754
|
+
function usage() {
|
|
755
|
+
return `Usage: node .ai/harness/writable-behavior-oracles.mjs --root <absolute-app-path> --case <OMH-NNN> --phase <before|after> [--json]`
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function parseArgs(argv) {
|
|
759
|
+
const options = { root: undefined, caseId: undefined, phase: undefined, json: false, internalCase: undefined }
|
|
760
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
761
|
+
const arg = argv[index]
|
|
762
|
+
const takeValue = () => {
|
|
763
|
+
const value = argv[index + 1]
|
|
764
|
+
if (!value || value.startsWith('--')) throw new Error(`${arg} requires a value`)
|
|
765
|
+
index += 1
|
|
766
|
+
return value
|
|
767
|
+
}
|
|
768
|
+
if (arg === '--root') options.root = takeValue()
|
|
769
|
+
else if (arg === '--case') options.caseId = takeValue()
|
|
770
|
+
else if (arg === '--phase') options.phase = takeValue()
|
|
771
|
+
else if (arg === '--json') options.json = true
|
|
772
|
+
else if (arg === '--internal-case') options.internalCase = takeValue()
|
|
773
|
+
else if (arg === '--help' || arg === '-h') options.help = true
|
|
774
|
+
else throw new Error(`unknown argument: ${arg}`)
|
|
775
|
+
}
|
|
776
|
+
return options
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function isPathInside(root, candidate) {
|
|
780
|
+
const relative = path.relative(root, candidate)
|
|
781
|
+
return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative))
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function safeMessage(error, root) {
|
|
785
|
+
let message = String(error?.message ?? error ?? 'unknown error')
|
|
786
|
+
for (const sensitive of [root, process.env.HOME, os.homedir()].filter(Boolean).sort((a, b) => b.length - a.length)) {
|
|
787
|
+
message = message.split(sensitive).join('<redacted-path>')
|
|
788
|
+
}
|
|
789
|
+
return message
|
|
790
|
+
.replace(/\b(?:sk|ghp|github_pat|xox[baprs])[-_A-Za-z0-9]{8,}\b/g, '<redacted-token>')
|
|
791
|
+
.slice(0, 500)
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function hasExportModifier(ts, node) {
|
|
795
|
+
return Boolean(node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword))
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function unwrapTypeExpression(ts, node) {
|
|
799
|
+
let current = node
|
|
800
|
+
while (current && (
|
|
801
|
+
ts.isParenthesizedExpression(current)
|
|
802
|
+
|| ts.isAsExpression(current)
|
|
803
|
+
|| ts.isTypeAssertionExpression(current)
|
|
804
|
+
|| ts.isNonNullExpression(current)
|
|
805
|
+
|| (typeof ts.isSatisfiesExpression === 'function' && ts.isSatisfiesExpression(current))
|
|
806
|
+
)) current = current.expression
|
|
807
|
+
return current
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function sourceDeclaresConcreteExport(ts, file, source, exportName) {
|
|
811
|
+
const ast = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, file.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS)
|
|
812
|
+
const localFunctions = new Set()
|
|
813
|
+
let direct = false
|
|
814
|
+
let exportedLocally = false
|
|
815
|
+
for (const statement of ast.statements) {
|
|
816
|
+
if (ts.isFunctionDeclaration(statement) && statement.name?.text === exportName) {
|
|
817
|
+
localFunctions.add(exportName)
|
|
818
|
+
if (hasExportModifier(ts, statement)) direct = true
|
|
819
|
+
continue
|
|
820
|
+
}
|
|
821
|
+
if (ts.isVariableStatement(statement)) {
|
|
822
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
823
|
+
if (!ts.isIdentifier(declaration.name) || declaration.name.text !== exportName) continue
|
|
824
|
+
const initializer = declaration.initializer ? unwrapTypeExpression(ts, declaration.initializer) : undefined
|
|
825
|
+
if (!initializer || (!ts.isArrowFunction(initializer) && !ts.isFunctionExpression(initializer))) continue
|
|
826
|
+
localFunctions.add(exportName)
|
|
827
|
+
if (hasExportModifier(ts, statement)) direct = true
|
|
828
|
+
}
|
|
829
|
+
continue
|
|
830
|
+
}
|
|
831
|
+
if (!ts.isExportDeclaration(statement) || statement.moduleSpecifier || !statement.exportClause || !ts.isNamedExports(statement.exportClause)) continue
|
|
832
|
+
exportedLocally ||= statement.exportClause.elements.some((element) =>
|
|
833
|
+
(element.propertyName?.text ?? element.name.text) === exportName && element.name.text === exportName)
|
|
834
|
+
}
|
|
835
|
+
return direct || (exportedLocally && localFunctions.has(exportName))
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function findExportSource(root, caseRecord) {
|
|
839
|
+
const ts = loadTargetTypeScript(root)
|
|
840
|
+
const directory = path.resolve(root, caseRecord.directory)
|
|
841
|
+
if (!isPathInside(root, directory)) throw new Error('oracle target directory escapes the app root')
|
|
842
|
+
const realRoot = fs.realpathSync(root)
|
|
843
|
+
const realDirectory = fs.realpathSync(directory)
|
|
844
|
+
if (!isPathInside(realRoot, realDirectory)) throw new Error('oracle target directory resolves outside the app root')
|
|
845
|
+
const candidates = []
|
|
846
|
+
const pending = [realDirectory]
|
|
847
|
+
while (pending.length) {
|
|
848
|
+
const current = pending.pop()
|
|
849
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
850
|
+
if (entry.isSymbolicLink()) throw new Error('oracle target directory contains a symbolic link')
|
|
851
|
+
const absolute = path.join(current, entry.name)
|
|
852
|
+
if (entry.isDirectory()) {
|
|
853
|
+
pending.push(absolute)
|
|
854
|
+
continue
|
|
855
|
+
}
|
|
856
|
+
if (!entry.isFile() || !/\.[cm]?tsx?$/.test(entry.name) || /\.(?:d|test|spec)\.[cm]?tsx?$/.test(entry.name)) continue
|
|
857
|
+
const stat = fs.statSync(absolute)
|
|
858
|
+
if (stat.size > MAX_SOURCE_BYTES) throw new Error(`oracle target exceeds ${MAX_SOURCE_BYTES} bytes`)
|
|
859
|
+
const source = fs.readFileSync(absolute, 'utf8')
|
|
860
|
+
if (sourceDeclaresConcreteExport(ts, absolute, source, caseRecord.requiredExport)) candidates.push({ absolute, source })
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
if (candidates.length !== 1) {
|
|
864
|
+
throw new Error(`oracle target must contain exactly one exported ${caseRecord.requiredExport}; found ${candidates.length}`)
|
|
865
|
+
}
|
|
866
|
+
return candidates[0]
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function readTargetSource(root, caseRecord) {
|
|
870
|
+
if (caseRecord.directory) {
|
|
871
|
+
const match = findExportSource(root, caseRecord)
|
|
872
|
+
return { source: match.source, file: path.relative(root, match.absolute) }
|
|
873
|
+
}
|
|
874
|
+
const candidate = path.resolve(root, caseRecord.file)
|
|
875
|
+
if (!isPathInside(root, candidate)) throw new Error('oracle target escapes the app root')
|
|
876
|
+
const realRoot = fs.realpathSync(root)
|
|
877
|
+
const realCandidate = fs.realpathSync(candidate)
|
|
878
|
+
if (!isPathInside(realRoot, realCandidate)) throw new Error('oracle target resolves outside the app root')
|
|
879
|
+
const stat = fs.statSync(realCandidate)
|
|
880
|
+
if (!stat.isFile()) throw new Error(`oracle target is not a file: ${caseRecord.file}`)
|
|
881
|
+
if (stat.size > MAX_SOURCE_BYTES) throw new Error(`oracle target exceeds ${MAX_SOURCE_BYTES} bytes`)
|
|
882
|
+
return { source: fs.readFileSync(realCandidate, 'utf8'), file: caseRecord.file }
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function loadTargetTypeScript(root) {
|
|
886
|
+
const targetRequire = createRequire(path.join(root, 'package.json'))
|
|
887
|
+
let resolved
|
|
888
|
+
try { resolved = targetRequire.resolve('typescript') } catch { throw new Error('target app TypeScript compiler is unavailable') }
|
|
889
|
+
return targetRequire(resolved)
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function transpileTarget(root, caseRecord, source) {
|
|
893
|
+
const ts = loadTargetTypeScript(root)
|
|
894
|
+
const result = ts.transpileModule(source, {
|
|
895
|
+
fileName: caseRecord.file,
|
|
896
|
+
reportDiagnostics: true,
|
|
897
|
+
compilerOptions: {
|
|
898
|
+
module: ts.ModuleKind.CommonJS,
|
|
899
|
+
target: ts.ScriptTarget.ES2022,
|
|
900
|
+
jsx: ts.JsxEmit.React,
|
|
901
|
+
esModuleInterop: true,
|
|
902
|
+
},
|
|
903
|
+
})
|
|
904
|
+
const diagnostics = (result.diagnostics ?? []).filter((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error)
|
|
905
|
+
if (diagnostics.length) {
|
|
906
|
+
const codes = diagnostics.slice(0, 8).map((diagnostic) => `TS${diagnostic.code}`).join(', ')
|
|
907
|
+
throw new Error(`target TypeScript compilation failed (${codes})`)
|
|
908
|
+
}
|
|
909
|
+
let output = result.outputText
|
|
910
|
+
let importStub = ''
|
|
911
|
+
if (caseRecord.allowCompiledImports) {
|
|
912
|
+
importStub = `
|
|
913
|
+
const __harnessPublicAddress = '1.1.1.1'
|
|
914
|
+
const __harnessDnsPromises = Object.freeze({
|
|
915
|
+
async lookup(hostname, options) {
|
|
916
|
+
const address = hostname === 'localhost' ? '127.0.0.1' : hostname === '[::1]' ? '::1' : hostname === 'metadata.internal' ? '169.254.169.254' : /^\\d+(?:\\.\\d+){3}$/.test(hostname) ? hostname : __harnessPublicAddress
|
|
917
|
+
return options?.all ? [{ address, family: address.includes(':') ? 6 : 4 }] : { address, family: address.includes(':') ? 6 : 4 }
|
|
918
|
+
},
|
|
919
|
+
})
|
|
920
|
+
const __harnessDns = Object.freeze({
|
|
921
|
+
lookup(hostname, options, callback) {
|
|
922
|
+
const resolvedCallback = typeof options === 'function' ? options : callback
|
|
923
|
+
const address = hostname === 'localhost' ? '127.0.0.1' : hostname === '[::1]' ? '::1' : hostname === 'metadata.internal' ? '169.254.169.254' : /^\\d+(?:\\.\\d+){3}$/.test(hostname) ? hostname : __harnessPublicAddress
|
|
924
|
+
if (typeof options === 'object' && options?.all) resolvedCallback(null, [{ address, family: address.includes(':') ? 6 : 4 }])
|
|
925
|
+
else resolvedCallback(null, address, address.includes(':') ? 6 : 4)
|
|
926
|
+
},
|
|
927
|
+
})
|
|
928
|
+
const __harnessNet = Object.freeze({
|
|
929
|
+
isIP(value) {
|
|
930
|
+
if (/^\\d+(?:\\.\\d+){3}$/.test(value)) return 4
|
|
931
|
+
if (String(value).includes(':')) return 6
|
|
932
|
+
return 0
|
|
933
|
+
},
|
|
934
|
+
})
|
|
935
|
+
const __harnessImportStub = new Proxy(function (...args) {
|
|
936
|
+
const callback = args.find((argument) => typeof argument === 'function')
|
|
937
|
+
return callback ? callback() : __harnessImportStub
|
|
938
|
+
}, {
|
|
939
|
+
get(_target, property) { return property === 'then' ? undefined : __harnessImportStub },
|
|
940
|
+
construct() { return __harnessImportStub },
|
|
941
|
+
})
|
|
942
|
+
`
|
|
943
|
+
output = output
|
|
944
|
+
.replace(/\brequire\((['"])node:dns\/promises\1\)/g, '__harnessDnsPromises')
|
|
945
|
+
.replace(/\brequire\((['"])node:dns\1\)/g, '__harnessDns')
|
|
946
|
+
.replace(/\brequire\((['"])node:net\1\)/g, '__harnessNet')
|
|
947
|
+
output = output.replace(/\brequire\((['"])[^'"]+\1\)/g, '__harnessImportStub')
|
|
948
|
+
} else if (caseRecord.allowedCompiledImport) {
|
|
949
|
+
const escaped = caseRecord.allowedCompiledImport.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
950
|
+
const importExpression = new RegExp(`require\\(["']${escaped}["']\\)`, 'g')
|
|
951
|
+
output = output.replace(importExpression, '({ CrudForm: function CrudForm() {} })')
|
|
952
|
+
}
|
|
953
|
+
if (/\brequire\s*\(/.test(output)) throw new Error('behavior-oracle target must not execute imports')
|
|
954
|
+
return `${importStub}${output}`
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function executeWorker(caseId, compiledSource) {
|
|
958
|
+
const child = spawnSync(process.execPath, [
|
|
959
|
+
'--permission', path.resolve(process.argv[1]), '--internal-case', caseId,
|
|
960
|
+
], {
|
|
961
|
+
input: compiledSource,
|
|
962
|
+
encoding: 'utf8',
|
|
963
|
+
timeout: WORKER_TIMEOUT_MS,
|
|
964
|
+
maxBuffer: 512 * 1024,
|
|
965
|
+
env: { NO_COLOR: '1' },
|
|
966
|
+
})
|
|
967
|
+
if (child.error?.code === 'ETIMEDOUT' || child.signal) throw new Error('behavior probe timed out')
|
|
968
|
+
if (child.error) throw child.error
|
|
969
|
+
if (child.status !== 0) throw new Error(child.stderr.trim() || 'behavior probe worker failed')
|
|
970
|
+
let parsed
|
|
971
|
+
try { parsed = JSON.parse(child.stdout) } catch { throw new Error('behavior probe worker returned invalid JSON') }
|
|
972
|
+
if (!Array.isArray(parsed?.checks) || parsed.checks.some((check) => typeof check?.id !== 'string' || typeof check?.passed !== 'boolean')) {
|
|
973
|
+
throw new Error('behavior probe worker returned an invalid result')
|
|
974
|
+
}
|
|
975
|
+
return parsed
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
async function internalRun(caseId) {
|
|
979
|
+
const caseRecord = CASES[caseId]
|
|
980
|
+
const probeScript = PROBES[caseRecord?.probeCase ?? caseId] ?? FAMILY_PROBES[caseRecord?.family]
|
|
981
|
+
if (!probeScript) throw new Error(`unsupported behavior-oracle case: ${caseId}`)
|
|
982
|
+
const chunks = []
|
|
983
|
+
let size = 0
|
|
984
|
+
for await (const chunk of process.stdin) {
|
|
985
|
+
size += chunk.length
|
|
986
|
+
if (size > MAX_SOURCE_BYTES * 4) throw new Error('compiled target exceeds worker input limit')
|
|
987
|
+
chunks.push(chunk)
|
|
988
|
+
}
|
|
989
|
+
const compiledSource = Buffer.concat(chunks).toString('utf8')
|
|
990
|
+
const context = vm.createContext(Object.create(null), {
|
|
991
|
+
name: `writable-behavior-oracle-${caseId}`,
|
|
992
|
+
codeGeneration: { strings: false, wasm: false },
|
|
993
|
+
})
|
|
994
|
+
new vm.Script(`
|
|
995
|
+
'use strict';
|
|
996
|
+
class OracleURLSearchParams {
|
|
997
|
+
#entries = []
|
|
998
|
+
constructor(query = '') {
|
|
999
|
+
for (const pair of String(query).replace(/^\\?/, '').split('&')) {
|
|
1000
|
+
if (!pair) continue
|
|
1001
|
+
const separator = pair.indexOf('=')
|
|
1002
|
+
const key = separator < 0 ? pair : pair.slice(0, separator)
|
|
1003
|
+
const value = separator < 0 ? '' : pair.slice(separator + 1)
|
|
1004
|
+
this.#entries.push([decodeURIComponent(key), decodeURIComponent(value)])
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
get(key) { return this.#entries.find(([name]) => name === String(key))?.[1] ?? null }
|
|
1008
|
+
set(key, value) {
|
|
1009
|
+
const name = String(key)
|
|
1010
|
+
this.#entries = this.#entries.filter(([candidate]) => candidate !== name)
|
|
1011
|
+
this.#entries.push([name, String(value)])
|
|
1012
|
+
}
|
|
1013
|
+
toString() { return this.#entries.map(([key, value]) => encodeURIComponent(key) + '=' + encodeURIComponent(value)).join('&') }
|
|
1014
|
+
}
|
|
1015
|
+
class OracleURL {
|
|
1016
|
+
constructor(input) {
|
|
1017
|
+
const match = /^(https?):\\/\\/([^/?#]+)([^?#]*)(?:\\?([^#]*))?(?:#(.*))?$/.exec(String(input))
|
|
1018
|
+
if (!match) throw new TypeError('invalid absolute URL')
|
|
1019
|
+
this.protocol = match[1] + ':'
|
|
1020
|
+
const authority = match[2]
|
|
1021
|
+
const bracketEnd = authority.startsWith('[') ? authority.indexOf(']') : -1
|
|
1022
|
+
const portIndex = bracketEnd > -1
|
|
1023
|
+
? (authority[bracketEnd + 1] === ':' ? bracketEnd + 1 : -1)
|
|
1024
|
+
: authority.lastIndexOf(':')
|
|
1025
|
+
this.hostname = bracketEnd > -1 ? authority.slice(0, bracketEnd + 1) : portIndex > -1 ? authority.slice(0, portIndex) : authority
|
|
1026
|
+
this.port = portIndex > -1 ? authority.slice(portIndex + 1) : ''
|
|
1027
|
+
this.pathname = match[3] || '/'
|
|
1028
|
+
this.hash = match[5] ? '#' + match[5] : ''
|
|
1029
|
+
this.searchParams = new OracleURLSearchParams(match[4] || '')
|
|
1030
|
+
}
|
|
1031
|
+
toString() {
|
|
1032
|
+
const query = this.searchParams.toString()
|
|
1033
|
+
return this.protocol + '//' + this.hostname + (this.port ? ':' + this.port : '') + this.pathname + (query ? '?' + query : '') + this.hash
|
|
1034
|
+
}
|
|
1035
|
+
get href() { return this.toString() }
|
|
1036
|
+
}
|
|
1037
|
+
globalThis.URL = OracleURL
|
|
1038
|
+
globalThis.URLSearchParams = OracleURLSearchParams
|
|
1039
|
+
globalThis.module = { exports: Object.create(null) }
|
|
1040
|
+
globalThis.exports = globalThis.module.exports
|
|
1041
|
+
`).runInContext(context, { timeout: 250 })
|
|
1042
|
+
new vm.Script(`'use strict';\n${compiledSource}`, { filename: caseRecord.file }).runInContext(context, { timeout: 1_000 })
|
|
1043
|
+
Object.defineProperties(context, {
|
|
1044
|
+
__oracleExportName: { value: caseRecord.exportName ?? null },
|
|
1045
|
+
__oracleMode: { value: caseRecord.mode ?? null },
|
|
1046
|
+
__oracleRequiredFlagsJson: { value: JSON.stringify(caseRecord.requiredFlags ?? []) },
|
|
1047
|
+
})
|
|
1048
|
+
ORACLE_CONFIG_SCRIPT.runInContext(context, { timeout: 250 })
|
|
1049
|
+
probeScript.runInContext(context, { timeout: 1_000 })
|
|
1050
|
+
const serialized = await new vm.Script(`(async () => JSON.stringify(await globalThis.__oracleResult))()`).runInContext(context, { timeout: 1_000 })
|
|
1051
|
+
process.stdout.write(`${serialized}\n`)
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function emit(result, json) {
|
|
1055
|
+
if (json) process.stdout.write(`${JSON.stringify(result)}\n`)
|
|
1056
|
+
else {
|
|
1057
|
+
const marker = result.passed ? 'PASS' : 'FAIL'
|
|
1058
|
+
process.stdout.write(`${marker}: ${result.failures.join('; ') || 'all behavior checks passed'}\n`)
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
async function main() {
|
|
1063
|
+
let options
|
|
1064
|
+
try { options = parseArgs(process.argv.slice(2)) } catch (error) {
|
|
1065
|
+
console.error(error.message)
|
|
1066
|
+
console.error(usage())
|
|
1067
|
+
return EXIT_INVALID
|
|
1068
|
+
}
|
|
1069
|
+
if (options.internalCase) {
|
|
1070
|
+
try { await internalRun(options.internalCase); return EXIT_PASS } catch (error) {
|
|
1071
|
+
console.error(safeMessage(error))
|
|
1072
|
+
return EXIT_INVALID
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
if (options.help) { console.log(usage()); return EXIT_PASS }
|
|
1076
|
+
const root = options.root ? path.resolve(options.root) : undefined
|
|
1077
|
+
try {
|
|
1078
|
+
if (!options.root || !path.isAbsolute(options.root)) throw new Error('--root must be an absolute path')
|
|
1079
|
+
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) throw new Error('--root must identify an existing directory')
|
|
1080
|
+
if (!CASES[options.caseId]) throw new Error(`--case must be one of: ${Object.keys(CASES).join(', ')}`)
|
|
1081
|
+
if (!['before', 'after'].includes(options.phase)) throw new Error('--phase must be before or after')
|
|
1082
|
+
const caseRecord = CASES[options.caseId]
|
|
1083
|
+
const target = readTargetSource(root, caseRecord)
|
|
1084
|
+
const compiled = transpileTarget(root, { ...caseRecord, file: target.file }, target.source)
|
|
1085
|
+
const probe = executeWorker(options.caseId, compiled)
|
|
1086
|
+
const failures = probe.checks.filter((check) => !check.passed).map((check) => `${check.id}: behavior requirement was not met`)
|
|
1087
|
+
const result = { passed: failures.length === 0, failures, checks: probe.checks }
|
|
1088
|
+
emit(result, options.json)
|
|
1089
|
+
return result.passed ? EXIT_PASS : EXIT_FAILURE
|
|
1090
|
+
} catch (error) {
|
|
1091
|
+
emit({ passed: false, failures: [safeMessage(error, root)], checks: [] }, options.json)
|
|
1092
|
+
return EXIT_INVALID
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
process.exitCode = await main()
|