Files
KVideo/verification/tests/harness/action.test.mjs
T
kuekhaoyang 3771b63332 feat: add self-contained strict verification chain
Keep every post-59948b5 change inside verification/. The verifier owns its npm working directory and sanitized Docker context, so no release metadata or root Docker configuration is modified.
2026-08-02 06:11:13 +08:00

147 lines
7.3 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { assessAction } from '../../src/browser/action-effects.mjs';
import { fillInput } from '../../src/browser/action-state.mjs';
import { clickControl, reorderSortable, stateDifference, stateSnapshot } from '../../src/browser/actions.mjs';
import { semanticDifference } from '../../src/browser/semantic.mjs';
import { BROWSER_FIXTURE_ORIGIN } from '../../src/browser/init.mjs';
import { requestFailureBucket } from '../../src/browser/session.mjs';
import { parseAndroidVersion } from '../../src/checks/android-config.mjs';
import { coverageArgs } from '../../src/checks/coverage.mjs';
import { projectSourceFile } from '../../src/checks/source-policy.mjs';
function evidence(overrides = {}) {
return {
url: '/', localStorage: [], sessionStorage: [], clipboard: '', media: [],
dom: { hash: 'dom', items: [] }, actionState: { hash: 'state', signatures: [] },
observed: { requests: 0, responses: 0, dialogs: 0, downloads: 0, popups: 0,
consoleErrors: 0, pageErrors: 0, failedRequests: 0, httpErrors: 0 },
...overrides,
};
}
test('action state hashes ignore origin and locator paths but preserve semantic controls', () => {
const left = stateSnapshot('http://127.0.0.1:1/settings', [{ signature: 'button|save|0' }]);
const right = stateSnapshot('https://example.test/settings', [{ signature: 'button|save|0' }]);
assert.equal(left.hash, right.hash);
assert.deepEqual(stateDifference(left, { ...right, signatures: ['button|cancel|0'] }), {
missing: ['button|save|0'], unexpected: ['button|cancel|0'],
});
});
test('state graphs distinguish one duplicate but bound unbounded identical instances', () => {
const once = stateSnapshot('https://example.test/', [{ signature: 'button|tag|0' }]);
const twice = stateSnapshot('https://example.test/', [{ signature: 'button|tag|0' }, { signature: 'button|tag|1' }]);
const thrice = stateSnapshot('https://example.test/', [
{ signature: 'button|tag|0' }, { signature: 'button|tag|1' }, { signature: 'button|tag|2' },
]);
assert.notEqual(once.hash, twice.hash);
assert.equal(twice.hash, thrice.hash);
});
test('state graphs preserve sortable order for deterministic replay', () => {
const left = stateSnapshot('https://example.test/', [
{ signature: 'tag|popular|0', roleDescription: 'sortable', path: 'main>div:1' },
{ signature: 'tag|drama|0', roleDescription: 'sortable', path: 'main>div:2' },
]);
const right = stateSnapshot('https://example.test/', [
{ signature: 'tag|drama|0', roleDescription: 'sortable', path: 'main>div:1' },
{ signature: 'tag|popular|0', roleDescription: 'sortable', path: 'main>div:2' },
]);
assert.notEqual(left.hash, right.hash);
});
test('silent controls fail while storage changes are independently observable', () => {
const action = { aria: '保存', text: '保存' };
const before = evidence();
const silent = assessAction(action, { ok: true, operation: 'click' }, before, evidence());
assert.equal(silent.ok, false);
assert.equal(silent.failureKind, 'no-effect');
const changed = assessAction(action, { ok: true, operation: 'click' }, before,
evidence({ localStorage: [['saved', 'true']] }));
assert.equal(changed.ok, true);
assert.deepEqual(changed.effects, ['localStorage']);
});
test('selected choices and current-location links are explicit idempotent actions', () => {
const before = evidence({ url: '/settings' });
const selected = assessAction({ selected: true }, { ok: true }, before, before);
assert.equal(selected.idempotent, true);
const link = assessAction({ href: '/settings' }, { ok: true }, before, before);
assert.equal(link.idempotent, true);
});
test('blank-target clicks arm popup capture before clicking and preserve the final URL', async () => {
let armed = false;
let closed = false;
const popup = { waitForLoadState: async () => {}, url: () => 'https://example.test/result',
close: async () => { closed = true; } };
const page = { waitForEvent: async (name) => { assert.equal(name, 'popup'); armed = true; return popup; } };
const locator = { click: async () => { assert.equal(armed, true); } };
const result = await clickControl(page, locator, '_blank');
assert.deepEqual(result, { operation: 'click', popupUrl: 'https://example.test/result' });
assert.equal(closed, true);
});
test('sortable controls skip singleton groups and use the keyboard sensor otherwise', async () => {
const singleton = await reorderSortable({ locator: () => ({ count: async () => 1 }) }, {});
assert.equal(singleton.idempotent, true);
const keys = [];
let order = ['|one', '|two'];
const peers = { count: async () => 2, evaluateAll: async (_callback, targetId) => targetId
? { index: 0, order: [...order] } : [...order] };
const page = { locator: () => peers, keyboard: { press: async (key) => {
keys.push(key); if (key === 'ArrowRight') order = ['|two', '|one'];
} }, waitForTimeout: async () => {} };
const locator = { getAttribute: async () => '7', focus: async () => {} };
const moved = await reorderSortable(page, locator);
assert.equal(moved.operation, 'keyboard-sort');
assert.deepEqual(moved.afterOrder, ['|two', '|one']);
assert.deepEqual(keys, ['Space', 'ArrowRight', 'Space']);
});
test('intentional browser request cancellation is recorded separately from failures', () => {
assert.equal(requestFailureBucket('net::ERR_ABORTED'), 'abortedRequests');
assert.equal(requestFailureBucket('net::ERR_CONNECTION_REFUSED'), 'failedRequests');
});
test('seek controls require directional media proof', () => {
const before = evidence({ media: [{ currentTime: 20, paused: true, muted: true, volume: 1, playbackRate: 1, currentSrc: 'x' }] });
const after = evidence({ media: [{ currentTime: 10, paused: true, muted: true, volume: 1, playbackRate: 1, currentSrc: 'x' }] });
const result = assessAction({ aria: '后退 10 秒', text: '' }, { ok: true }, before, after);
assert.equal(result.ok, true);
assert.equal(result.proof.kind, 'seek-backward');
});
test('URL inputs use the local interceptable import fixture', async () => {
let value = '';
await fillInput({ fill: async (next) => { value = next; } }, 'url');
assert.equal(value, `${BROWSER_FIXTURE_ORIGIN}/source-import.json`);
});
test('filling an input with its existing deterministic value is idempotent', async () => {
const result = await fillInput({ inputValue: async () => '验证', fill: async () => {} }, 'text');
assert.equal(result.idempotent, true);
});
test('semantic deltas expose missing and unexpected visible controls', () => {
assert.deepEqual(semanticDifference({ items: ['a', 'b'] }, { items: ['b', 'c'] }), {
missing: ['a'], unexpected: ['c'],
});
});
test('coverage, Android, and line policies are strict by construction', () => {
const ctx = { config: { verifyDir: '/verify' } };
const args = coverageArgs(ctx, '/reports');
assert.ok(args.includes('--all'));
assert.ok(args.includes('--100'));
assert.ok(args.includes('app'));
assert.ok(args.includes('components'));
assert.ok(args.includes('lib'));
assert.ok(args.includes('scripts'));
assert.deepEqual(parseAndroidVersion('versionCode = 9\nversionName = "4.9.20"'), { versionName: '4.9.20', versionCode: 9 });
assert.equal(projectSourceFile('/repo/app-release.json'), true);
assert.equal(projectSourceFile('/repo/Dockerfile'), true);
assert.equal(projectSourceFile('/repo/package-lock.json'), false);
});