Files
sanderling/pkg/spec/test/defaults.test.ts
pj eed99e58aa refactor: code organization cleanup (#35)
* chore: fix gitignore + decisions doc after web->inspect-ui rename

Update web/ references to inspect-ui/ in .gitignore and Makefile. Add
decisions.md tracking architectural decisions from code-org discussion.

* refactor: rename pkg/spec-api to pkg/spec

Aligns the directory name with the npm package name @sanderling/spec.
Updates Makefile, package.json directory field, and resolveSpecAPIPath.

* refactor(verifier): split bindings.go into types.go + bindings.go

Move shared public types (Action, ActionKind, LogEntry, Exception) to
types.go. bindings.go retains internal JS runtime wiring only.

* refactor(inspect): split runs.go into runs.go, runs_cache.go, runs_decode.go

runs.go: types (RunSummary, StepSummary, RunDetail, Run) and Scan.
runs_cache.go: Cache type, Open/Step/Detail methods, parseRun, scanSteps.
runs_decode.go: readMeta, tallyTrace, decodeStepSummary, validRunID.

* refactor: move android_env.go to internal/android/

Extracts Android device/AVD/adb logic into internal/android package.
Exports EnsureDevice, AdbReverse, AdbReverseRemove, EnvWithAndroidPlatformTools, AdbBinary.
Moves tests to internal/android/android_test.go. cmd/sanderling becomes a thin caller.

* refactor: extract test pipeline to internal/testrun/

runTestPipeline logic moves to testrun.Execute. buildDriver, resolveSpecAPIPath,
pickFreePort, and the progress logger move to internal/testrun/. cmd/sanderling/test_run.go
becomes a thin adapter. Tests follow their code.

* ci: update workflow paths after pkg/spec-api -> pkg/spec rename
2026-04-22 20:35:34 +07:00

111 lines
3.8 KiB
TypeScript

import assert from "node:assert/strict";
import { test } from "node:test";
import type {
Action,
ActionGenerator,
EventuallyFormula,
Extracted,
Formula,
Sampler,
State,
SanderlingRuntime,
WeightedEntry,
} from "../src/types.ts";
interface RecordedRuntime extends SanderlingRuntime {
currentState: State;
extractors: Array<(state: State) => unknown>;
alwaysArgs: Array<(() => boolean) | Formula>;
lastPredicate: (() => boolean) | undefined;
}
function installRuntime(initialState: State): RecordedRuntime {
const extractors: Array<(state: State) => unknown> = [];
const extracted: Array<{ value: unknown }> = [];
const alwaysArgs: Array<(() => boolean) | Formula> = [];
let lastPredicate: (() => boolean) | undefined;
const runtime = {
extract: <T>(getter: (state: State) => T): Extracted<T> => {
extractors.push(getter as (state: State) => unknown);
const slot = { value: getter(state.currentState) };
extracted.push(slot);
return {
get current(): T {
return slot.value as T;
},
previous: undefined,
};
},
always: (predicateOrFormula: (() => boolean) | Formula): Formula => {
alwaysArgs.push(predicateOrFormula);
if (typeof predicateOrFormula === "function") {
lastPredicate = predicateOrFormula;
}
return { __sanderlingFormula: true } as Formula;
},
now: () => ({ __sanderlingFormula: true } as Formula),
next: () => ({ __sanderlingFormula: true } as Formula),
eventually: () => ({ __sanderlingFormula: true } as EventuallyFormula),
actions: (generator: () => Action[]): ActionGenerator => ({
__sanderlingActionGenerator: true,
generate: generator,
}),
weighted: (..._entries: WeightedEntry[]): ActionGenerator => ({
__sanderlingActionGenerator: true,
generate: () => [],
}),
from: <T>(_items: readonly T[]): Sampler<T> => ({ generate: () => _items[0] as T }),
tap: ({ on }) => ({ kind: "Tap", on }),
inputText: ({ into, text }) => ({ kind: "InputText", into, text }),
swipe: (p) => ({ kind: "Swipe", from: p.from, to: p.to, durationMillis: p.durationMillis }),
pressKey: ({ key }) => ({ kind: "PressKey", key }),
wait: ({ durationMillis }) => ({ kind: "Wait", durationMillis }),
taps: { __sanderlingActionGenerator: true, generate: () => [] } as ActionGenerator,
swipes: { __sanderlingActionGenerator: true, generate: () => [] } as ActionGenerator,
waitOnce: { __sanderlingActionGenerator: true, generate: () => [] } as ActionGenerator,
pressKeys: { __sanderlingActionGenerator: true, generate: () => [] } as ActionGenerator,
} satisfies SanderlingRuntime;
const state = { currentState: initialState };
const recorded = Object.assign(runtime, {
currentState: initialState,
extractors,
alwaysArgs,
get lastPredicate() {
return lastPredicate;
},
}) as unknown as RecordedRuntime;
globalThis.__sanderling__ = recorded;
// Re-bind state ref so subsequent extract() calls read the up-to-date state.
Object.defineProperty(recorded, "currentState", {
get() {
return state.currentState;
},
set(next: State) {
state.currentState = next;
},
});
return recorded;
}
const emptyState: State = {
snapshots: {},
ax: { find: () => undefined, findAll: () => [] },
lastAction: null,
time: 0,
logs: [],
exceptions: [],
};
test("defaults bundle exports formulas tagged as LTL properties", async () => {
installRuntime({
...emptyState,
logs: [{ unixMillis: 1, level: "W", tag: "X", message: "warn" }],
});
const defaults = await import("../src/defaults/properties.ts");
assert.equal(defaults.noUncaughtExceptions.__sanderlingFormula, true);
assert.equal(defaults.noLogcatErrors.__sanderlingFormula, true);
});