From 42eae65508d09f4a372a60bb3cfa22655a89701b Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sat, 15 Aug 2026 01:16:21 -0400 Subject: [PATCH] feat: add adversarial server clock control --- docs/adversarial-runtime.md | 39 +++++++++-- packages/cli/src/adversarial-recipe.ts | 40 ++++++++++- .../runtime-core/src/adversarial-browser.ts | 2 +- .../runtime-core/src/adversarial-campaign.ts | 51 ++++++++++++-- packages/runtime-core/src/recipe-schema.ts | 23 ++++++- .../runtime-core/src/runtime-contracts.ts | 2 +- .../src/browser-clock-control.ts | 1 + packages/runtime-playground/src/index.ts | 2 +- .../src/wordpress-adversarial-adapter.ts | 69 +++++++++++++++++-- tests/adversarial-campaign.test.ts | 69 ++++++++++++++++++- .../adversarial-recipe-orchestration.test.ts | 38 +++++++++- tests/wordpress-adversarial-adapter.test.ts | 23 +++++++ 12 files changed, 334 insertions(+), 25 deletions(-) diff --git a/docs/adversarial-runtime.md b/docs/adversarial-runtime.md index dde9d8a91..d1218f3ad 100644 --- a/docs/adversarial-runtime.md +++ b/docs/adversarial-runtime.md @@ -149,10 +149,38 @@ removed in reverse order after success, failure, cancellation, or timeout. ## Clock control Playwright browser time supports exact freeze, advance, skew, and resume through -its clock API. The same capability response explicitly marks server process, -scheduler, and database clocks unsupported. A WordPress runtime extension is -required to control those surfaces without faking server behavior in browser -JavaScript. +its clock API. WordPress Playground additionally supports a disposable, +opt-in server clock seam for recipe campaigns. A normalized clock transition is +declared on an adversarial action and applied immediately before that action's +action phase. Setup phases remain grouped before action execution, so a later +transition cannot advance time before an earlier action runs. This preserves +before/after expiry boundaries and is copied with +the action into replay evidence. + +```json +"actions": [ + { "type": "assert-before-expiry", "clock": [{ "surface": "scheduler", "operation": "freeze", "time": 1900000000000 }] }, + { "type": "assert-after-expiry", "clock": [{ "surface": "scheduler", "operation": "advance", "milliseconds": 1000 }] } +] +``` + +The Playground seam exposes `wp_codebox_adversarial_clock_now()`, +`wp_codebox_adversarial_clock_timestamp()`, and +`wp_codebox_adversarial_clock_datetime()` for code paths that explicitly choose +deterministic campaign time. It supplies the scheduler's explicit due-event +timestamp. These WordPress and scheduler surfaces are **emulated**. Native PHP +`time()`, `DateTime`, WordPress `current_time()`/`current_datetime()` consumers +that do not use the seam, and database clock functions remain **unsupported**; +the adapter never claims native interception. Browser clocks remain a separate +Playwright capability; server clock action transitions intentionally exclude the +browser surface rather than claiming mixed-clock coordination. + +Clock transition negotiation is backend-specific. The WordPress Playground +adapter is selected only when the active runtime reports that backend; neutral +or other backends fail closed before campaign cases begin. If a case does not +settle within the bounded abort grace after timing out, the campaign is marked +incomplete and no subsequent case is scheduled on that runtime. Runtime teardown +owns any non-cooperative execution that remains alive. ## Browser oracles @@ -243,7 +271,8 @@ The following capabilities are intentionally not claimed by this change: - exact socket framing faults require a lower-level proxy provider; - server-side WordPress HTTP fault interception requires a WordPress extension adapter using the generic fault contract; -- PHP/WordPress, cron, and database clock control require a WordPress extension; +- native PHP/WordPress and database clock interception are unsupported; the + WordPress scheduler seam is an explicitly emulated capability; - WordPress-specific mutation grammars, security policies, and instrumentation remain extension-owned; - live vulnerable plugin/theme discovery campaigns require disposable runtime diff --git a/packages/cli/src/adversarial-recipe.ts b/packages/cli/src/adversarial-recipe.ts index 5176bae4b..16884791d 100644 --- a/packages/cli/src/adversarial-recipe.ts +++ b/packages/cli/src/adversarial-recipe.ts @@ -24,6 +24,7 @@ import { type WorkspaceRecipeStep, } from "@automattic/wp-codebox-core" import { stripUndefined } from "@automattic/wp-codebox-core/internals" +import { negotiateWordPressServerClock, wordpressServerClockCleanupAction, wordpressServerClockScheduleAction, type WordPressServerClockNegotiation } from "@automattic/wp-codebox-playground" import type { InputMountPathMapping } from "./input-mount-paths.js" import { recipeAdversarialCapabilities } from "./recipe-validation.js" @@ -37,6 +38,7 @@ export interface RecipeAdversarialCampaignOutput { available: string[] required: string[] optional: Array<{ id: string; available: boolean }> + clock?: WordPressServerClockNegotiation } evidence?: Awaited> } @@ -79,6 +81,15 @@ export async function runRecipeAdversarialCampaigns(options: RunRecipeAdversaria await options.runtime.createCheckpoint({ name: checkpointName, metadata: { campaignId: declaration.id, immutableBaseline: true } }) } const templates = new Map(declaration.caseTemplates.map((template) => [template.id, template])) + const clockTransitions = declaration.corpus.flatMap(({ actions }) => actions.flatMap((action) => action.clock ?? [])) + const runtimeInfo = clockTransitions.length > 0 ? await options.runtime.info() : undefined + if (clockTransitions.length > 0 && runtimeInfo?.backend !== "wordpress-playground") { + throw new Error(`Recipe adversarial campaign ${declaration.id} requires server clock transitions, but runtime backend ${runtimeInfo?.backend ?? "unknown"} does not provide the WordPress Playground clock adapter.`) + } + const clockNegotiation = clockTransitions.length > 0 ? negotiateWordPressServerClock(clockTransitions) : undefined + if (clockNegotiation && !clockNegotiation.supported) { + throw new Error(`Recipe adversarial campaign ${declaration.id} has unsupported clock transitions: ${clockNegotiation.unsupported.map(({ surface, operation, reason }) => `${surface}.${operation} (${reason})`).join(", ")}`) + } const campaign = adversarialCampaign({ id: declaration.id, seed: declaration.seed, @@ -101,7 +112,17 @@ export async function runRecipeAdversarialCampaigns(options: RunRecipeAdversaria const campaignOptions = { ...options, executions: campaignExecutions } const execute = async (plan: AdversarialCasePlan, signal: AbortSignal) => { if (checkpointName) await options.runtime.restoreCheckpoint!(checkpointName) - return executeRecipeAdversarialCase(declaration, templates, plan, signal, campaignOptions) + try { + return await executeRecipeAdversarialCase(declaration, templates, plan, signal, campaignOptions) + } finally { + if (plan.actions.some((action) => action.clock?.length)) { + const cleanup = wordpressServerClockCleanupAction() + const execution = await options.runtime.execute(cleanup) + campaignExecutions.push(execution) + } + // Checkpoints isolate all runtime state when they are available. + if (checkpointName) await options.runtime.restoreCheckpoint!(checkpointName) + } } const result = replay ? await runRecipeAdversarialReplay(campaign, declaration, templates, replay, execute, options.signal) @@ -120,6 +141,7 @@ export async function runRecipeAdversarialCampaigns(options: RunRecipeAdversaria available: capabilities, required: declaration.requiredCapabilities ?? [], optional: (declaration.optionalCapabilities ?? []).map((id) => ({ id, available: capabilities.includes(id) })), + ...(clockNegotiation ? { clock: clockNegotiation } : {}), }, }) } @@ -272,8 +294,20 @@ function stableAdversarialDiagnosticMessage(message: string, caseId: string): st function materializeCasePhases(plan: AdversarialCasePlan, templates: Map): Partial> { const phases: Partial> = {} - for (const phase of ["setup", "action", "assert", "teardown"] as const) { - phases[phase] = plan.actions.flatMap((action) => (templates.get(action.type)?.phases[phase] ?? []).map((step) => materializeStep(step, plan, action.input))) + for (const action of plan.actions) { + const template = templates.get(action.type) + if (action.clock?.length && (template?.phases.action?.length ?? 0) === 0) { + throw new Error(`Clock transition for adversarial action ${action.type} requires at least one action-phase step.`) + } + for (const phase of ["setup", "action", "assert", "teardown"] as const) { + const steps = (template?.phases[phase] ?? []).map((step) => materializeStep(step, plan, action.input)) + if (phase === "action" && action.clock?.length) { + const transition = wordpressServerClockScheduleAction(action.clock) + phases[phase] = [...(phases[phase] ?? []), { command: transition.command, args: transition.args, metadata: transition.metadata }, ...steps] + } else { + phases[phase] = [...(phases[phase] ?? []), ...steps] + } + } } return phases } diff --git a/packages/runtime-core/src/adversarial-browser.ts b/packages/runtime-core/src/adversarial-browser.ts index 4e9b62296..7f0925231 100644 --- a/packages/runtime-core/src/adversarial-browser.ts +++ b/packages/runtime-core/src/adversarial-browser.ts @@ -38,7 +38,7 @@ export interface AdversarialBrowserOracleResult { } export interface ClockControlCapability { - surface: "runtime" | "browser" | "scheduler" | "database" + surface: "runtime" | "wordpress" | "browser" | "scheduler" | "database" freeze: boolean advance: boolean skew: boolean diff --git a/packages/runtime-core/src/adversarial-campaign.ts b/packages/runtime-core/src/adversarial-campaign.ts index a29fedd0b..8977b50a6 100644 --- a/packages/runtime-core/src/adversarial-campaign.ts +++ b/packages/runtime-core/src/adversarial-campaign.ts @@ -12,10 +12,22 @@ export const DIFFERENTIAL_RESULT_SCHEMA = "wp-codebox/differential-result/v1" as export type AdversarialMutationKind = "scalar" | "structured" | "binary" | "sequence" export type AdversarialCaseStatus = "passed" | "failed" | "error" | "timed-out" | "resource-exhausted" +export type AdversarialClockSurface = "runtime" | "wordpress" | "scheduler" | "database" +export type AdversarialClockOperation = "freeze" | "advance" | "skew" | "restore" + +/** A portable, normalized clock instruction. Runtime adapters negotiate its fidelity. */ +export interface AdversarialClockScheduleEntry { + surface: AdversarialClockSurface + operation: AdversarialClockOperation + time?: number + milliseconds?: number +} export interface AdversarialAction { type: string input?: unknown + /** Transitions applied immediately before this action's first declared phase. */ + clock?: AdversarialClockScheduleEntry[] metadata?: Record } @@ -158,6 +170,8 @@ export interface AdversarialCampaignRunnerOptions { signal?: AbortSignal retainNovelty?: boolean minimize?: boolean + /** Grace to wait for a cancelled case before terminalizing the campaign. */ + abortSettleGraceMs?: number } export interface DifferentialCell { @@ -230,6 +244,11 @@ export async function runAdversarialCampaign(campaignInput: AdversarialCampaign, const observation = observations[index] as AdversarialExecutionObservation executed += 1 if (observation.status === "timed-out") timedOut += 1 + if (observation.diagnostics?.some(({ code }) => code === "case-timeout-unsettled")) { + incomplete = true + diagnostics.push({ code: "campaign-timeout-unsettled", message: `Campaign stopped because ${plan.caseId} did not settle after cancellation. The runtime must be torn down before further work.` }) + break + } artifactBytes += (observation.artifacts ?? []).reduce((total, artifact) => total + (artifact.bytes ?? 0), 0) if (artifactBytes > campaign.budgets.maxArtifactBytes) { incomplete = true; diagnostics.push({ code: "campaign-artifact-budget-exhausted", message: "Campaign stopped before artifact evidence exceeded its byte budget." }); break } @@ -301,10 +320,19 @@ async function executeBoundedCase(campaign: AdversarialCampaign, plan: Adversari options.signal?.addEventListener("abort", abort, { once: true }) let timer: NodeJS.Timeout | undefined try { - return await Promise.race([ - options.execute(plan, controller.signal), - new Promise((resolve) => { timer = setTimeout(() => { controller.abort(); resolve({ status: "timed-out", diagnostics: [{ code: "case-time-budget-exhausted", message: `Case exceeded ${campaign.budgets.maxCaseTimeMs}ms.` }] }) }, campaign.budgets.maxCaseTimeMs) }), + const execution = options.execute(plan, controller.signal) + const completed = await Promise.race([ + execution.then((observation) => ({ kind: "completed" as const, observation })), + new Promise<{ kind: "timed-out" }>((resolve) => { timer = setTimeout(() => { controller.abort(); resolve({ kind: "timed-out" }) }, campaign.budgets.maxCaseTimeMs) }), + ]) + if (completed.kind === "completed") return completed.observation + // Do not start another case while a timed-out case can still mutate shared state. + const settled = await Promise.race([ + execution.then(() => true, () => true), + new Promise((resolve) => setTimeout(() => resolve(false), boundedInteger(options.abortSettleGraceMs, 1_000, 1, 60_000))), ]) + if (!settled) return { status: "timed-out", diagnostics: [{ code: "case-timeout-unsettled", message: `Case exceeded ${campaign.budgets.maxCaseTimeMs}ms and did not settle during the abort grace period. Further cases require runtime teardown.` }], metadata: { cancellation: "runtime-teardown-required" } } + return { status: "timed-out", diagnostics: [{ code: "case-time-budget-exhausted", message: `Case exceeded ${campaign.budgets.maxCaseTimeMs}ms and settled after cancellation.` }] } } catch (error) { return { status: "error", diagnostics: [{ code: "case-execution-error", message: error instanceof Error ? error.message : String(error) }] } } finally { @@ -391,6 +419,21 @@ function createFinding(campaign: AdversarialCampaign, plan: AdversarialCasePlan, } } +export function normalizeAdversarialClockSchedule(entries: readonly AdversarialClockScheduleEntry[]): AdversarialClockScheduleEntry[] { + return entries.map((entry, index) => { + if (!entry || !["runtime", "wordpress", "scheduler", "database"].includes(entry.surface)) throw new Error(`Clock schedule entry ${index} has an invalid surface.`) + if (!entry || !["freeze", "advance", "skew", "restore"].includes(entry.operation)) throw new Error(`Clock schedule entry ${index} has an invalid operation.`) + if ((entry.operation === "freeze" || entry.operation === "skew") && (!Number.isSafeInteger(entry.time) || (entry.time as number) < 0)) throw new Error(`Clock schedule ${entry.operation} entry ${index} requires a non-negative Unix millisecond time.`) + if (entry.operation === "advance" && (!Number.isSafeInteger(entry.milliseconds) || (entry.milliseconds as number) < 0)) throw new Error(`Clock schedule advance entry ${index} requires non-negative milliseconds.`) + if (entry.operation === "restore" && (entry.time !== undefined || entry.milliseconds !== undefined)) throw new Error(`Clock schedule restore entry ${index} cannot include time values.`) + return entry.operation === "advance" + ? { surface: entry.surface, operation: entry.operation, milliseconds: entry.milliseconds } + : entry.operation === "freeze" || entry.operation === "skew" + ? { surface: entry.surface, operation: entry.operation, time: entry.time } + : { surface: entry.surface, operation: entry.operation } + }) +} + function defaultOracleResults(observation: AdversarialExecutionObservation): AdversarialOracleResult[] { return observation.status === "passed" ? [] : [{ oracleId: "runtime-status", failed: true, code: observation.status, message: observation.diagnostics?.[0]?.message ?? `Runtime status was ${observation.status}.` }] } @@ -418,7 +461,7 @@ function normalizeBudgets(input: Partial | undefined) } function normalizeCorpusEntry(entry: AdversarialCorpusEntry): AdversarialCorpusEntry { - return stripUndefined({ ...entry, actions: entry.actions.map((action) => ({ ...action, input: cloneJsonValue(action.input) })), input: cloneJsonValue(entry.input), signals: entry.signals ? [...new Set(entry.signals)].sort() : undefined }) + return stripUndefined({ ...entry, actions: entry.actions.map((action) => stripUndefined({ ...action, input: cloneJsonValue(action.input), clock: action.clock ? normalizeAdversarialClockSchedule(action.clock) : undefined })), input: cloneJsonValue(entry.input), signals: entry.signals ? [...new Set(entry.signals)].sort() : undefined }) } function mutateActionSequence(actions: AdversarialAction[], seed: string, maximum: number): AdversarialAction[] { diff --git a/packages/runtime-core/src/recipe-schema.ts b/packages/runtime-core/src/recipe-schema.ts index ca11e590f..c0ca70e1b 100644 --- a/packages/runtime-core/src/recipe-schema.ts +++ b/packages/runtime-core/src/recipe-schema.ts @@ -1412,7 +1412,7 @@ export function createWorkspaceRecipeJsonSchema(options: WorkspaceRecipeJsonSche type: "object", additionalProperties: false, required: ["type"], - properties: { type: { type: "string", pattern: "^[A-Za-z0-9][A-Za-z0-9_.-]*$" }, input: {}, metadata: { $ref: "#/$defs/metadata" } }, + properties: { type: { type: "string", pattern: "^[A-Za-z0-9][A-Za-z0-9_.-]*$" }, input: {}, clock: { type: "array", minItems: 1, maxItems: 32, items: { $ref: "#/$defs/adversarialClockScheduleEntry" } }, metadata: { $ref: "#/$defs/metadata" } }, }, adversarialCaseTemplate: { type: "object", @@ -1468,6 +1468,27 @@ export function createWorkspaceRecipeJsonSchema(options: WorkspaceRecipeJsonSche metadata: { $ref: "#/$defs/metadata" }, }, }, + adversarialClockScheduleEntry: { + type: "object", + additionalProperties: false, + required: ["surface", "operation"], + allOf: [{ + if: { properties: { operation: { enum: ["freeze", "skew"] } }, required: ["operation"] }, + then: { required: ["time"] }, + }, { + if: { properties: { operation: { const: "advance" } }, required: ["operation"] }, + then: { required: ["milliseconds"] }, + }, { + if: { properties: { operation: { const: "restore" } }, required: ["operation"] }, + then: { not: { anyOf: [{ required: ["time"] }, { required: ["milliseconds"] }] } }, + }], + properties: { + surface: { enum: ["runtime", "wordpress", "scheduler", "database"] }, + operation: { enum: ["freeze", "advance", "skew", "restore"] }, + time: { type: "integer", minimum: 0 }, + milliseconds: { type: "integer", minimum: 0 }, + }, + }, transportFaultModel: { type: "object", additionalProperties: false, diff --git a/packages/runtime-core/src/runtime-contracts.ts b/packages/runtime-core/src/runtime-contracts.ts index b8d7d471e..41aafdc3f 100644 --- a/packages/runtime-core/src/runtime-contracts.ts +++ b/packages/runtime-core/src/runtime-contracts.ts @@ -368,7 +368,7 @@ export interface WorkspaceRecipeAdversarialCampaign { seed: string corpus: Array<{ id: string - actions: Array<{ type: string; input?: unknown; metadata?: Record }> + actions: Array<{ type: string; input?: unknown; clock?: import("./adversarial-campaign.js").AdversarialClockScheduleEntry[]; metadata?: Record }> input?: unknown signals?: string[] metadata?: Record diff --git a/packages/runtime-playground/src/browser-clock-control.ts b/packages/runtime-playground/src/browser-clock-control.ts index cd926d346..5a8b4de34 100644 --- a/packages/runtime-playground/src/browser-clock-control.ts +++ b/packages/runtime-playground/src/browser-clock-control.ts @@ -4,6 +4,7 @@ import type { Page } from "playwright" export const PLAYWRIGHT_CLOCK_CONTROL_CAPABILITIES: ClockControlCapabilities = clockControlCapabilities("playwright", [ { surface: "browser", freeze: true, advance: true, skew: true, restore: true, fidelity: "exact" }, { surface: "runtime", freeze: false, advance: false, skew: false, restore: false, fidelity: "unsupported", reason: "Browser clock control does not alter the server process clock." }, + { surface: "wordpress", freeze: false, advance: false, skew: false, restore: false, fidelity: "unsupported", reason: "Browser clock control does not alter WordPress API clock seams." }, { surface: "scheduler", freeze: false, advance: false, skew: false, restore: false, fidelity: "unsupported", reason: "Server scheduler control requires a runtime extension." }, { surface: "database", freeze: false, advance: false, skew: false, restore: false, fidelity: "unsupported", reason: "Browser clock control does not alter database time functions." }, ]) diff --git a/packages/runtime-playground/src/index.ts b/packages/runtime-playground/src/index.ts index f5822d25e..3ecb82989 100644 --- a/packages/runtime-playground/src/index.ts +++ b/packages/runtime-playground/src/index.ts @@ -19,7 +19,7 @@ export { browserPreviewAuthCookieUrls, browserPreviewNetworkPolicySummary, brows export { BROWSER_TRANSPORT_FAULT_CAPABILITIES, applyBrowserTransportFault, browserTransportFaultReport, createBrowserTransportFaultAdapter, installBrowserTransportFaults, type BrowserTransportFaultAdapter, type BrowserTransportFaultInstallOptions, type BrowserTransportFaultReport, type BrowserTransportFaultTeardown, type InstalledBrowserTransportFaults } from "./browser-transport-faults.js" export { PLAYWRIGHT_CLOCK_CONTROL_CAPABILITIES, createBrowserClockController, type BrowserClockController } from "./browser-clock-control.js" export { PLAYWRIGHT_BROWSER_ENVIRONMENT_CAPABILITIES, applyPlaywrightPageEnvironment, browserEnvironmentCell, createPlaywrightBrowserEnvironmentContext, observePlaywrightBrowserEnvironment, resolvePlaywrightBrowserEnvironment, runPlaywrightBrowserEnvironmentMatrix, type BrowserEnvironmentCpuProfile, type BrowserEnvironmentNetworkProfile, type PlaywrightBrowserEnvironmentExecutionInput, type PlaywrightBrowserEnvironmentOptions, type PlaywrightBrowserEnvironmentRuntime, type PlaywrightBrowserEnvironmentSession } from "./browser-environment-matrix.js" -export { WORDPRESS_ADVERSARIAL_ADAPTER_SCHEMA, WORDPRESS_ADVERSARIAL_CAPABILITIES, WORDPRESS_ADVERSARIAL_ORACLES, WORDPRESS_CLOCK_CONTROL_CAPABILITIES, WORDPRESS_HTTP_TRANSPORT_FAULT_CAPABILITIES, createWordPressAdversarialAdapter, evaluateWordPressAdversarialOracles, negotiateWordPressHttpTransportFaults, wordpressAdversarialActionSpec, wordpressHttpFaultConfigurationAction, wordpressNoveltySignals, wordpressSchedulerClockAction, type WordPressAdapterFidelity, type WordPressAdversarialAction, type WordPressAdversarialAdapter, type WordPressAdversarialCapability, type WordPressAdversarialSurface } from "./wordpress-adversarial-adapter.js" +export { WORDPRESS_ADVERSARIAL_ADAPTER_SCHEMA, WORDPRESS_ADVERSARIAL_CAPABILITIES, WORDPRESS_ADVERSARIAL_ORACLES, WORDPRESS_CLOCK_CONTROL_CAPABILITIES, WORDPRESS_HTTP_TRANSPORT_FAULT_CAPABILITIES, createWordPressAdversarialAdapter, evaluateWordPressAdversarialOracles, negotiateWordPressHttpTransportFaults, negotiateWordPressServerClock, wordpressAdversarialActionSpec, wordpressHttpFaultConfigurationAction, wordpressNoveltySignals, wordpressSchedulerClockAction, wordpressServerClockCleanupAction, wordpressServerClockScheduleAction, type WordPressAdapterFidelity, type WordPressAdversarialAction, type WordPressAdversarialAdapter, type WordPressAdversarialCapability, type WordPressAdversarialSurface, type WordPressServerClockNegotiation } from "./wordpress-adversarial-adapter.js" export { normalizePreviewReviewerAccess, previewReviewerAccess } from "./preview-reviewer-access.js" export { phpunitExecutionSemantics, requiresManagedMysqlMultisitePreinstall, type PhpunitExecutionSemantics } from "./phpunit-command-semantics.js" export { applyVfsMountSnapshots, materializePlaygroundMountsFromVfs, type HostMountSnapshot, type MountMaterializationResult, type VfsMountSnapshot } from "./mount-materialization.js" diff --git a/packages/runtime-playground/src/wordpress-adversarial-adapter.ts b/packages/runtime-playground/src/wordpress-adversarial-adapter.ts index 62817ae8b..85ed644c3 100644 --- a/packages/runtime-playground/src/wordpress-adversarial-adapter.ts +++ b/packages/runtime-playground/src/wordpress-adversarial-adapter.ts @@ -3,6 +3,7 @@ import { createHash } from "node:crypto" import { ADVERSARIAL_ORACLE_SCHEMA, clockControlCapabilities, + normalizeAdversarialClockSchedule, negotiateTransportFaults, transportFaultCapabilities, type AdversarialCasePlan, @@ -10,6 +11,7 @@ import { type AdversarialOracleContract, type AdversarialOracleResult, type ClockControlCapabilities, + type AdversarialClockScheduleEntry, type RuntimeEpisodeActionSpec, type TransportFaultCapabilities, type TransportFaultModel, @@ -59,6 +61,12 @@ export interface WordPressAdversarialAdapter { oracleIds: string[] } +export interface WordPressServerClockNegotiation { + supported: boolean + schedule: AdversarialClockScheduleEntry[] + unsupported: Array<{ surface: AdversarialClockScheduleEntry["surface"]; operation: AdversarialClockScheduleEntry["operation"]; reason: string }> +} + export const WORDPRESS_ADVERSARIAL_CAPABILITIES: readonly WordPressAdversarialCapability[] = [ { surface: "rest", fidelity: "exact", reason: "Dispatched through WP_REST_Request and rest_do_request()." }, { surface: "ajax", fidelity: "exact", reason: "Dispatched through the runtime HTTP server to wp-admin/admin-ajax.php, preserving HTTP and wp_die() termination semantics." }, @@ -96,9 +104,10 @@ const transportCapabilities = [ export const WORDPRESS_HTTP_TRANSPORT_FAULT_CAPABILITIES = transportFaultCapabilities("wordpress-http-api", [...transportCapabilities]) export const WORDPRESS_CLOCK_CONTROL_CAPABILITIES = clockControlCapabilities("wordpress-playground", [ - { surface: "runtime", freeze: false, advance: false, skew: false, restore: false, fidelity: "unsupported", reason: "PHP time(), current_time(), and current_datetime() have no global supported clock injection primitive in this runtime." }, + { surface: "runtime", freeze: false, advance: false, skew: false, restore: false, fidelity: "unsupported", reason: "PHP native time(), DateTime, and direct runtime clock consumers cannot be intercepted by a supported primitive." }, + { surface: "wordpress", freeze: true, advance: true, skew: true, restore: true, fidelity: "emulated", reason: "The opt-in WordPress clock seam is available to supported code paths; current_time() and current_datetime() are not intercepted." }, { surface: "browser", freeze: true, advance: true, skew: false, restore: true, fidelity: "exact", reason: "Use the existing Playwright clock controller for browser time." }, - { surface: "scheduler", freeze: true, advance: true, skew: true, restore: true, fidelity: "emulated", reason: "Campaigns select and invoke due cron events against an explicit timestamp; background spawn timing is not changed." }, + { surface: "scheduler", freeze: true, advance: true, skew: true, restore: true, fidelity: "emulated", reason: "The injected WordPress clock seam supplies explicit due-event timestamps; background spawning and native time consumers are unchanged." }, { surface: "database", freeze: false, advance: false, skew: false, restore: false, fidelity: "unsupported", reason: "The default SQLite runtime database clock is independent and exposes no supported injection primitive." }, ]) @@ -162,6 +171,47 @@ export function wordpressSchedulerClockAction(timestamp: number, hook: string, a return wordpressAdversarialActionSpec({ surface: "cron", operation: "run-due", target: hook, input: { timestamp, args } }) } +/** + * Installs the disposable WordPress clock seam and applies a normalized schedule. + * The seam is intentionally opt-in: it does not claim to intercept PHP time(). + */ +export function wordpressServerClockScheduleAction(entries: readonly AdversarialClockScheduleEntry[]): RuntimeEpisodeActionSpec { + const negotiation = negotiateWordPressServerClock(entries) + if (!negotiation.supported) throw new Error(`WordPress server clock negotiation failed: ${negotiation.unsupported.map(({ surface, operation, reason }) => `${surface}.${operation} (${reason})`).join(", ")}`) + const schedule = negotiation.schedule + const encodedSchedule = Buffer.from(JSON.stringify(schedule), "utf8").toString("base64") + const encodedPlugin = Buffer.from(wordpressServerClockMuPluginPhp(), "utf8").toString("base64") + return { + kind: "command", + command: "wordpress.run-php", + args: [`code=wp_mkdir_p(WPMU_PLUGIN_DIR); $__clock_plugin = WPMU_PLUGIN_DIR . '/wp-codebox-adversarial-clock.php'; file_put_contents($__clock_plugin, base64_decode('${encodedPlugin}')); if (!function_exists('wp_codebox_adversarial_clock_apply')) { require_once $__clock_plugin; } $__schedule = json_decode(base64_decode('${encodedSchedule}'), true); foreach ($__schedule as $__entry) { wp_codebox_adversarial_clock_apply($__entry); } echo wp_json_encode(array('schema' => 'wp-codebox/server-clock-application/v1', 'schedule' => $__schedule, 'fidelity' => array('runtime' => 'unsupported', 'wordpress' => 'emulated', 'scheduler' => 'emulated', 'database' => 'unsupported')));`], + operation: "adversarial:server-clock", + metadata: { adapter: WORDPRESS_ADVERSARIAL_ADAPTER_SCHEMA, clockSchedule: schedule, clockNegotiation: negotiation, fidelity: { runtime: "unsupported", wordpress: "emulated", scheduler: "emulated", database: "unsupported" } }, + } +} + +export function negotiateWordPressServerClock(entries: readonly AdversarialClockScheduleEntry[]): WordPressServerClockNegotiation { + const schedule = normalizeAdversarialClockSchedule(entries) + const unsupported = schedule.flatMap((entry) => { + const capability = WORDPRESS_CLOCK_CONTROL_CAPABILITIES.capabilities.find(({ surface }) => surface === entry.surface) + const enabled = capability?.[entry.operation] + return capability && capability.fidelity !== "unsupported" && enabled + ? [] + : [{ surface: entry.surface, operation: entry.operation, reason: capability?.reason ?? "surface is not declared by this adapter" }] + }) + return { supported: unsupported.length === 0, schedule, unsupported } +} + +export function wordpressServerClockCleanupAction(): RuntimeEpisodeActionSpec { + return { + kind: "command", + command: "wordpress.run-php", + args: ["code=delete_option('wp_codebox_adversarial_clock_state'); $___clock_plugin = WPMU_PLUGIN_DIR . '/wp-codebox-adversarial-clock.php'; if (file_exists($___clock_plugin)) { unlink($___clock_plugin); } echo wp_json_encode(array('schema' => 'wp-codebox/server-clock-cleanup/v1', 'status' => 'restored'));"], + operation: "adversarial:server-clock-cleanup", + metadata: { adapter: WORDPRESS_ADVERSARIAL_ADAPTER_SCHEMA, cleanup: true }, + } +} + export function wordpressHttpFaultConfigurationAction(model: TransportFaultModel): RuntimeEpisodeActionSpec { const negotiation = negotiateWordPressHttpTransportFaults(model) if (!negotiation.supported) { @@ -248,8 +298,8 @@ try { $candidate = wp_normalize_path($root . '/' . ltrim($target, '/')); $root_normalized = trailingslashit(wp_normalize_path($root)); if (strpos($candidate, $root_normalized) !== 0 || strpos($candidate, '..') !== false) { $result['status'] = 'denied'; $result['violations'][] = 'filesystem-escape-attempt'; break; } wp_mkdir_p(dirname($candidate)); $result['response'] = array('bytes' => file_put_contents($candidate, is_string($input) ? $input : wp_json_encode($input)), 'relativePath' => substr($candidate, strlen($root_normalized))); break; - case 'cron': - $clock = is_array($input) ? (int) ($input['timestamp'] ?? time()) : time(); $args = is_array($input['args'] ?? null) ? $input['args'] : array(); + case 'cron': + $clock = is_array($input) ? (int) ($input['timestamp'] ?? (function_exists('wp_codebox_adversarial_clock_timestamp') ? wp_codebox_adversarial_clock_timestamp() : time())) : (function_exists('wp_codebox_adversarial_clock_timestamp') ? wp_codebox_adversarial_clock_timestamp() : time()); $args = is_array($input['args'] ?? null) ? $input['args'] : array(); if ($request['operation'] === 'schedule') { $result['response'] = wp_schedule_single_event($clock, $target, $args, true); } else { $executed = 0; foreach ((array) _get_cron_array() as $timestamp => $hooks) { if ((int) $timestamp > $clock || empty($hooks[$target])) { continue; } foreach ($hooks[$target] as $event) { $event_args = (array) ($event['args'] ?? array()); do_action_ref_array($target, $event_args); wp_unschedule_event((int) $timestamp, $target, $event_args); $executed++; } } $result['response'] = array('hook' => $target, 'clock' => $clock, 'executed' => $executed); } break; case 'role-capability': @@ -308,6 +358,17 @@ add_filter('pre_http_request', static function ($preempt, $args, $url) { }, 10, 3);` } +function wordpressServerClockMuPluginPhp(): string { + return `setTimezone(wp_timezone()); } +function wp_codebox_adversarial_clock_apply($entry) { $entry = is_array($entry) ? $entry : array(); $operation = (string) ($entry['operation'] ?? ''); if ('restore' === $operation) { delete_option('wp_codebox_adversarial_clock_state'); return; } $state = wp_codebox_adversarial_clock_state(); if ('advance' === $operation) { $state['time'] = (int) ($state['time'] ?? floor(microtime(true) * 1000)) + max(0, (int) ($entry['milliseconds'] ?? 0)); } elseif ('freeze' === $operation || 'skew' === $operation) { $state['time'] = max(0, (int) ($entry['time'] ?? 0)); } else { throw new InvalidArgumentException('Unsupported adversarial clock operation.'); } update_option('wp_codebox_adversarial_clock_state', $state, false); } +` +} + function boundedFingerprint(value: unknown): string { return createHash("sha256").update(JSON.stringify(value)?.slice(0, 4096) ?? "undefined").digest("hex").slice(0, 16) } diff --git a/tests/adversarial-campaign.test.ts b/tests/adversarial-campaign.test.ts index 4100f4995..d2e552a3b 100644 --- a/tests/adversarial-campaign.test.ts +++ b/tests/adversarial-campaign.test.ts @@ -4,7 +4,7 @@ import { tmpdir } from "node:os" import { join } from "node:path" import test from "node:test" -import { adversarialCampaign, adversarialFindingFingerprint, classifyDifferentialResult, runAdversarialCampaign, type AdversarialCasePlan, type AdversarialExecutionObservation } from "../packages/runtime-core/src/adversarial-campaign.js" +import { adversarialCampaign, adversarialFindingFingerprint, classifyDifferentialResult, normalizeAdversarialClockSchedule, runAdversarialCampaign, type AdversarialCasePlan, type AdversarialExecutionObservation } from "../packages/runtime-core/src/adversarial-campaign.js" import { writeAdversarialEvidenceBundle } from "../packages/runtime-core/src/adversarial-artifacts.js" import { verifyArtifactBundle } from "../packages/runtime-core/src/artifact-bundle-verifier.js" @@ -70,6 +70,28 @@ test("finding fingerprints deduplicate equivalent failures independently of payl assert.equal(left, right) }) +test("action-bound clock transitions preserve expiry boundaries in replay evidence", async () => { + const clockSchedule = normalizeAdversarialClockSchedule([ + { surface: "scheduler", operation: "freeze", time: 1_900_000_000_000 }, + { surface: "scheduler", operation: "advance", milliseconds: 1_000 }, + ]) + const result = await runAdversarialCampaign(adversarialCampaign({ + id: "expiry-boundary", + seed: "expiry-boundary-seed", + corpus: [{ id: "expiry", actions: [ + { type: "assert-before-expiry", clock: [clockSchedule[0]!] }, + { type: "assert-after-expiry", clock: [clockSchedule[1]!] }, + ] }], + mutationKinds: ["scalar"], + budgets: { maxCases: 1, maxCaseTimeMs: 1_000, maxWallTimeMs: 5_000 }, + }), { + minimize: false, + execute: async () => ({ status: "failed", diagnostics: [{ code: "expired-at-boundary", message: "The supported scheduler seam observed expiry after 1000ms." }] }), + }) + assert.deepEqual(result.findings[0]?.replay.actions.map((action) => action.clock), clockSchedule.map((entry) => [entry])) + assert.throws(() => normalizeAdversarialClockSchedule([{ surface: "scheduler", operation: "advance", milliseconds: -1 }]), /non-negative/) +}) + test("differential matrices classify regressions and platform differences", () => { assert.equal(classifyDifferentialResult([ { id: "php-83-base", role: "base", status: "passed" }, @@ -106,7 +128,8 @@ test("campaign interruption stops scheduling and returns bounded partial evidenc const result = await runAdversarialCampaign(adversarialCampaign({ id: "interrupted", seed: "interrupted-seed", - corpus: [{ id: "seed", actions: [{ type: "observe" }] }], + corpus: [{ id: "seed", actions: [{ type: "observe", input: {} }] }], + mutationKinds: ["scalar"], budgets: { maxCases: 100, workers: 2, maxCaseTimeMs: 1000, maxWallTimeMs: 10000, maxArtifactBytes: 1024 }, }), { signal: controller.signal, @@ -125,6 +148,48 @@ test("campaign interruption stops scheduling and returns bounded partial evidenc assert(result.diagnostics.some((diagnostic) => diagnostic.code === "campaign-interrupted")) }) +test("a timed-out case settles before the next case starts", async () => { + const starts: string[] = [] + const settled: string[] = [] + const result = await runAdversarialCampaign(adversarialCampaign({ + id: "timeout-isolation", + seed: "timeout-isolation-seed", + corpus: [{ id: "seed", actions: [{ type: "observe", input: {} }] }], + mutationKinds: ["scalar"], + budgets: { maxCases: 2, workers: 1, maxCaseTimeMs: 5, maxWallTimeMs: 1_000 }, + }), { + execute: async (plan, signal) => { + starts.push(plan.caseId) + await new Promise((resolve) => signal.addEventListener("abort", resolve, { once: true })) + settled.push(plan.caseId) + return { status: "passed" } + }, + }) + assert.equal(result.summary.timedOut, 2) + assert.deepEqual(starts, settled, "the next case cannot begin while the timed-out case remains active") +}) + +test("an uncooperative timed-out case terminalizes the campaign without starting another case", async () => { + let started = 0 + const result = await runAdversarialCampaign(adversarialCampaign({ + id: "timeout-unsettled", + seed: "timeout-unsettled-seed", + corpus: [{ id: "seed", actions: [{ type: "observe", input: {} }] }], + mutationKinds: ["scalar"], + budgets: { maxCases: 2, workers: 1, maxCaseTimeMs: 5, maxWallTimeMs: 1_000 }, + }), { + abortSettleGraceMs: 10, + execute: async () => { + started += 1 + await new Promise(() => {}) + return { status: "passed" } + }, + }) + assert.equal(started, 1) + assert.equal(result.status, "incomplete") + assert(result.diagnostics.some(({ code }) => code === "campaign-timeout-unsettled")) +}) + test("minimization preserves the exact oracle and state fingerprint", async () => { const exactState = adversarialCampaign({ id: "exact-state", diff --git a/tests/adversarial-recipe-orchestration.test.ts b/tests/adversarial-recipe-orchestration.test.ts index eec8d4bf0..9234e5ea4 100644 --- a/tests/adversarial-recipe-orchestration.test.ts +++ b/tests/adversarial-recipe-orchestration.test.ts @@ -15,10 +15,14 @@ const recipe: WorkspaceRecipe = { schema: "wp-codebox/adversarial-recipe-campaign/v1", id: "neutral-state", seed: "deterministic-seed", - corpus: [{ id: "seed", actions: [{ type: "option-roundtrip", input: { value: "alpha" } }], input: { state: 1 }, signals: ["seed"] }], + corpus: [{ id: "seed", actions: [ + { type: "option-roundtrip", input: { value: "before-expiry" }, clock: [{ surface: "scheduler", operation: "freeze", time: 1_900_000_000_000 }] }, + { type: "option-roundtrip", input: { value: "after-expiry" }, clock: [{ surface: "scheduler", operation: "advance", milliseconds: 1_000 }] }, + ], input: { state: 1 }, signals: ["seed"] }], caseTemplates: [{ id: "option-roundtrip", phases: { + setup: [{ command: "wordpress.run-php", args: ["code=echo 'setup';"] }], action: [{ command: "wordpress.run-php", args: ["code=echo '{{action.input}}';"] }], assert: [{ command: "wordpress.run-php", args: ["code=echo '{{case.id}}';"] }], }, @@ -40,9 +44,10 @@ assert.deepEqual(await validateWorkspaceRecipeSemantics(recipe, "recipe.json"), assert(recipePolicy(recipe).commands.includes("wordpress.run-php"), "template commands must participate in policy derivation") const executions: ExecutionSpec[] = [] +const recipeExecutions: RecipeExecutionResult[] = [] const checkpointOperations: string[] = [] const runtime = { - info: async () => ({ id: "neutral", backend: "neutral", environment: { kind: "wordpress", name: "Neutral" }, createdAt: "2026-01-01T00:00:00.000Z", status: "created" }), + info: async () => ({ id: "playground", backend: "wordpress-playground", environment: { kind: "wordpress", name: "Playground" }, createdAt: "2026-01-01T00:00:00.000Z", status: "created" }), execute: async (spec: ExecutionSpec) => { executions.push(spec) return { id: `execution-${executions.length}`, command: spec.command, args: spec.args ?? [], exitCode: 0, stdout: "ok\n", stderr: "", startedAt: "2026-01-01T00:00:00.000Z", finishedAt: "2026-01-01T00:00:00.001Z" } @@ -62,7 +67,7 @@ const executeCampaign = async () => runRecipeAdversarialCampaigns({ recipePath: "/portable/recipe.json", recipeDirectory: "/portable", runtime, - executions: [], + executions: recipeExecutions, provenance: { runtime: "neutral" }, }) @@ -74,6 +79,13 @@ assert.deepEqual(first[0]?.result.schedule, second[0]?.result.schedule) assert.deepEqual(first[0]?.result.findings, second[0]?.result.findings) assert.equal(first[0]?.capabilities.optional[0]?.available, false, "optional fidelity must be explicit") assert(executions.length > 0, "generated cases must execute through runtime commands") +const clockedSuiteArg = recipeExecutions.flatMap((execution) => execution.args).find((arg) => arg.startsWith("input-json=")) +const clockedPhase = JSON.parse((clockedSuiteArg ?? "input-json={}").slice("input-json=".length)).cases[0].phases.action as Array<{ metadata?: { clockSchedule?: Array<{ operation: string }> }; args?: string[] }> +assert.equal(clockedPhase[0]?.metadata?.clockSchedule?.[0]?.operation, "freeze", "freeze runs before the pre-expiry action") +assert.match(clockedPhase[1]?.args?.[0] ?? "", /before-expiry/) +assert.equal(clockedPhase[2]?.metadata?.clockSchedule?.[0]?.operation, "advance", "advance runs before the post-expiry action") +assert.match(clockedPhase[3]?.args?.[0] ?? "", /after-expiry/) +assert(executions.some((execution) => execution.command === "wordpress.run-php" && execution.args.some((arg) => arg.includes("server-clock-cleanup"))), "clock state is cleaned after every case") assert(checkpointOperations.includes("create:baseline") && checkpointOperations.includes("restore:baseline"), "campaign cases must use the existing checkpoint reset path") assert(checkpointOperations.filter((operation) => operation === "restore:baseline").length >= (first[0]?.result.summary.generated ?? 0) + (second[0]?.result.summary.generated ?? 0), "every campaign and minimization execution must restore the declared baseline before running") @@ -85,6 +97,26 @@ const unsafeFaultRecipe = structuredClone(recipe) unsafeFaultRecipe.adversarialCampaigns![0]!.faultSchedule = { schema: "wp-codebox/transport-fault-model/v1", seed: "faults", rules: [] } assert.throws(() => validateWorkspaceRecipeShape(unsafeFaultRecipe, "faults.json"), /faultSchedule requires the transport-faults capability/) +const unsupportedClockRecipe = structuredClone(recipe) +unsupportedClockRecipe.adversarialCampaigns![0]!.corpus[0]!.actions[0]!.clock = [{ surface: "runtime", operation: "freeze", time: 1 }] +await assert.rejects(() => runRecipeAdversarialCampaigns({ recipe: unsupportedClockRecipe, recipePath: "/portable/unsupported-clock.json", recipeDirectory: "/portable", runtime, executions: [] }), /runtime\.freeze/) + +const neutralClockRuntime = { ...runtime, info: async () => ({ id: "neutral", backend: "neutral", environment: { kind: "wordpress", name: "Neutral" }, createdAt: "2026-01-01T00:00:00.000Z", status: "created" }) } as unknown as Runtime +await assert.rejects(() => runRecipeAdversarialCampaigns({ recipe, recipePath: "/portable/neutral-clock.json", recipeDirectory: "/portable", runtime: neutralClockRuntime, executions: [] }), /runtime backend neutral/) + +const noResetRecipe = structuredClone(recipe) +noResetRecipe.adversarialCampaigns![0]!.resetPolicy = { mode: "none" } +const noResetExecutions: ExecutionSpec[] = [] +const failedClockRuntime = { + ...runtime, + execute: async (spec: ExecutionSpec) => { + noResetExecutions.push(spec) + return { id: `no-reset-${noResetExecutions.length}`, command: spec.command, args: spec.args ?? [], exitCode: 1, stdout: "", stderr: "failed", startedAt: "2026-01-01T00:00:00.000Z", finishedAt: "2026-01-01T00:00:00.001Z" } + }, +} as unknown as Runtime +await runRecipeAdversarialCampaigns({ recipe: noResetRecipe, recipePath: "/portable/no-reset.json", recipeDirectory: "/portable", runtime: failedClockRuntime, executions: [] }) +assert(noResetExecutions.some((execution) => execution.args.some((arg) => arg.includes("server-clock-cleanup"))), "failed no-reset cases clean injected clock state") + assert.equal(resolveAdversarialReplayPath("files/replay.json", "/workspace"), "/workspace/files/replay.json") assert.throws(() => resolveAdversarialReplayPath("../replay.json", "/workspace"), /escapes the invocation workspace/) assert.throws(() => resolveAdversarialReplayPath("/outside/replay.json", "/workspace"), /escapes the invocation workspace/) diff --git a/tests/wordpress-adversarial-adapter.test.ts b/tests/wordpress-adversarial-adapter.test.ts index 01a479852..c22c70d05 100644 --- a/tests/wordpress-adversarial-adapter.test.ts +++ b/tests/wordpress-adversarial-adapter.test.ts @@ -8,10 +8,13 @@ import { createWordPressAdversarialAdapter, evaluateWordPressAdversarialOracles, negotiateWordPressHttpTransportFaults, + negotiateWordPressServerClock, wordpressAdversarialActionSpec, wordpressHttpFaultConfigurationAction, wordpressNoveltySignals, wordpressSchedulerClockAction, + wordpressServerClockScheduleAction, + wordpressServerClockCleanupAction, } from "../packages/runtime-playground/src/wordpress-adversarial-adapter.js" const adapter = createWordPressAdversarialAdapter() @@ -20,7 +23,9 @@ assert.equal(WORDPRESS_ADVERSARIAL_CAPABILITIES.find(({ surface }) => surface == assert.equal(WORDPRESS_ADVERSARIAL_CAPABILITIES.find(({ surface }) => surface === "ajax")?.fidelity, "exact") assert.equal(WORDPRESS_ADVERSARIAL_CAPABILITIES.find(({ surface }) => surface === "xmlrpc")?.fidelity, "exact") assert.equal(WORDPRESS_CLOCK_CONTROL_CAPABILITIES.capabilities.find(({ surface }) => surface === "runtime")?.fidelity, "unsupported") +assert.equal(WORDPRESS_CLOCK_CONTROL_CAPABILITIES.capabilities.find(({ surface }) => surface === "wordpress")?.fidelity, "emulated") assert.equal(WORDPRESS_CLOCK_CONTROL_CAPABILITIES.capabilities.find(({ surface }) => surface === "scheduler")?.fidelity, "emulated") +assert.match(WORDPRESS_CLOCK_CONTROL_CAPABILITIES.capabilities.find(({ surface }) => surface === "runtime")?.reason ?? "", /time\(\)/) const rest = wordpressAdversarialActionSpec({ surface: "rest", operation: "POST", target: "/fixture/v1/action", input: { value: "mutated" } }) assert.equal(rest.command, "wordpress.run-php") @@ -42,6 +47,24 @@ const scheduler = wordpressSchedulerClockAction(1900000000, "fixture_hook") assert.equal(scheduler.operation, "adversarial:cron") assert.equal(scheduler.metadata?.fidelity, "exact") +const clockSchedule = wordpressServerClockScheduleAction([ + { surface: "scheduler", operation: "freeze", time: 1_900_000_000_000 }, + { surface: "scheduler", operation: "advance", milliseconds: 1_000 }, +]) +assert.equal(clockSchedule.command, "wordpress.run-php") +assert.match(clockSchedule.args?.[0] ?? "", /wp-codebox-adversarial-clock\.php/) +assert.match(clockSchedule.args?.[0] ?? "", /wp_codebox_adversarial_clock_apply/) +assert.deepEqual(clockSchedule.metadata?.clockSchedule, [ + { surface: "scheduler", operation: "freeze", time: 1_900_000_000_000 }, + { surface: "scheduler", operation: "advance", milliseconds: 1_000 }, +]) +assert.equal((clockSchedule.metadata?.fidelity as Record).runtime, "unsupported") +assert.equal((clockSchedule.metadata?.fidelity as Record).scheduler, "emulated") +assert.throws(() => wordpressServerClockScheduleAction([{ surface: "database", operation: "freeze", time: 1 }]), /database/) +assert.equal(negotiateWordPressServerClock([{ surface: "runtime", operation: "freeze", time: 1 }]).supported, false) +assert.match(negotiateWordPressServerClock([{ surface: "runtime", operation: "freeze", time: 1 }]).unsupported[0]?.reason ?? "", /time\(\)/) +assert.match(wordpressServerClockCleanupAction().args?.[0] ?? "", /delete_option/) + const emulated = negotiateWordPressHttpTransportFaults(transportFaultModel({ seed: "faults", rules: [{ id: "timeout", match: { host: "fixture.invalid" }, sequence: [{ timeoutMs: 100 }] }],