v3.22.0: the test suites (scripts grouped, ordered, with setup/teardown)
A suite is the root object: every script belongs to exactly one suite, the
suite holds the ordered member list by reference - the optional setup first,
the members, the optional teardown last - and runs them in one card session,
without resets and without resume.
- test_suites.py: the suite store (test_suites.json, --test-suites), validated
and atomic like the script store: roles normalised (setup first, teardown
last, at most one of each, a script appears once per suite), per-member
on_fail stop/continue and the suite-level require_adm.
- test_scripts.py schema v2: suite_id/require_adm; list_by_suite()/orphans();
the import reports the id_map a bundle import remaps with. v3.21.0 scripts
without a suite are attached once to an auto-created "Imported scripts"
suite (migrate_scripts_to_suites, called from __main__).
- API: /api/test/suites (+ update|delete|import - delete refused while
non-empty; the bundle import imports the scripts first, remaps the suite
references and skips unresolvable ones); the scripts endpoints gain
suite_id/require_adm plus move|copy (delete detaches from the suite first;
a suite update refuses a membership change).
- runner: /api/test/run {suite_id} resolves the members and the preset, runs
the ADM gate, then setup -> members (per-member on_fail) -> teardown. The
teardown always runs on a stop or a failure (skipped only when the card is
gone; it clears the stop flag for its duration); a card reset stops the
suite, skips the remaining members and is flagged. Members' live steps land
in the shared snapshot, their records (status/steps/note/log excerpt/
counters) in _TEST_RUN['suite']; the summary carries the counts, wall time,
preset, the SCP80 counters before/after per keyset and the session flags.
_test_log buffers the semantic lines (bounded) for the report's per-member
excerpts.
- ADM gate (_ensure_adm): a suite/script require_adm verifies the matched
preset's key once per card session (the /api/verify-adm badge sets the same
latch); a failure refuses the start, stamps the session (no automatic
retry) and needs a manual verify.
- PWA: the Test script pill becomes four views - the suite list (default),
the suite editor (the script list: role badges, on_fail, up/down, per-script
Run/Edit/Move/Copy/Delete, Add script with the templates), the script editor
(the step list; a single run shows its result under the steps and is not
preserved) and the suite report (per-member blocks with the step table,
notes and run log; summary; export JSON/Markdown). Suite bundles export/
import; the legacy script export imports into "Imported scripts". EN/RU.
- tests: the suite store/migration, the runner orchestration (order, on_fail,
setup failure, stop, card reset, teardown, per-member logs) and the ADM gate
(Python); the suite HTTP paths; the PWA report/Markdown/wiring (Node).
Tests: 833 frontend + 794 Python; sw cache simple-v397.
This commit is contained in:
@@ -40,6 +40,8 @@ eval(extractFunc(html, 'testStepSummary'));
|
||||
eval(extractFunc(html, 'testCommandOptions'));
|
||||
globalThis.t = s => s;
|
||||
globalThis.esc = s => String(s);
|
||||
globalThis.testScripts = () => _testScripts || [];
|
||||
globalThis.testSuites = () => _testSuites || [];
|
||||
eval(extractFunc(html, 'testFormRow'));
|
||||
eval(extractFunc(html, 'testFormInput'));
|
||||
eval(extractFunc(html, 'testFormSelect'));
|
||||
@@ -53,11 +55,14 @@ eval(extractFunc(html, 'testScriptsWriteback', true));
|
||||
eval(extractFunc(html, 'testScriptsWritebackQueued'));
|
||||
eval(extractFunc(html, 'testScriptsSave'));
|
||||
eval(extractFunc(html, 'testRunStart', true));
|
||||
eval(extractFunc(html, 'testDelete', true));
|
||||
eval(extractFunc(html, 'testSuiteDeleteScript', true));
|
||||
eval(extractFunc(html, 'testSuitesRefetch', true));
|
||||
eval(extractFunc(html, 'testChecksCollect'));
|
||||
eval('var _testEditStep = null; var _testEditChecks = []; var _testEditStepIndex = -1;'
|
||||
+ ' var _testScripts = null; var _testCurrentIdx = -1; var _testRunState = null;'
|
||||
+ ' var _testSaveTimer = null; var _testWriteChains = new WeakMap(); var _testLastPresetIdx = -1;');
|
||||
+ ' var _testScripts = null; var _testSuites = []; var _testScriptId = null;'
|
||||
+ ' var _testScriptDraft = null; var _testRunState = null; var _testSaveTimer = null;'
|
||||
+ ' var _testWriteChains = new WeakMap(); var _testLastPresetIdx = -1;'
|
||||
+ ' var _testRunScriptRef = null; var _testSuiteIdx = -1;');
|
||||
globalThis.localStorage = { getItem: () => null, setItem: () => {}, removeItem: () => {} };
|
||||
|
||||
function fakeForm(values) {
|
||||
@@ -443,12 +448,14 @@ test('two quick saves of a draft create one script, then update it', async () =>
|
||||
};
|
||||
globalThis.ioStatus = () => {};
|
||||
_testScripts = [{ name: 'one', steps: [{ type: 'action', kind: 'status', params: { attempts: 1 } }] }];
|
||||
_testCurrentIdx = 0;
|
||||
globalThis.testCurrent = () => _testScripts[_testCurrentIdx];
|
||||
_testScriptId = null;
|
||||
globalThis.testCurrentScript = () => _testScripts[0];
|
||||
testScriptsSave(true);
|
||||
testScriptsSave(true);
|
||||
await new Promise(r => setTimeout(r, 40));
|
||||
assert.deepStrictEqual(calls, ['/api/test/scripts', '/api/test/scripts/update']);
|
||||
// the create is followed by a suites refetch (the suite gained the member)
|
||||
assert.deepStrictEqual(calls, ['/api/test/scripts', '/api/test/suites',
|
||||
'/api/test/scripts/update']);
|
||||
assert.strictEqual(_testScripts[0].id, 'd'.repeat(32));
|
||||
});
|
||||
|
||||
@@ -458,15 +465,21 @@ test('deleting a script cancels a pending debounced save', async () => {
|
||||
globalThis.ioStatus = () => {};
|
||||
globalThis.confirm = () => true;
|
||||
globalThis.testRender = () => {};
|
||||
_testScripts = [{ name: 'draft', steps: [{ type: 'action', kind: 'status', params: { attempts: 1 } }] }];
|
||||
_testCurrentIdx = 0;
|
||||
globalThis.testCurrent = () => _testScripts[_testCurrentIdx];
|
||||
globalThis.testStoresFetch = async () => {};
|
||||
const script = {id: 'e'.repeat(32), name: 'edited',
|
||||
steps: [{type: 'action', kind: 'status', params: {attempts: 1}}]};
|
||||
_testScripts = [script];
|
||||
globalThis.testCurrentScript = () => script;
|
||||
globalThis.testScriptById = () => script;
|
||||
globalThis.testCurrentSuite = () => ({id: 'a'.repeat(32),
|
||||
scripts: [{script_id: script.id, role: 'member', on_fail: 'stop'}]});
|
||||
globalThis.testSuiteEntry = () => ({script_id: script.id});
|
||||
globalThis.testSuites = () => [];
|
||||
testScriptsSave(); // schedules the 600 ms write
|
||||
await testDelete(); // a draft: local removal only
|
||||
await testSuiteDeleteScript(0); // the delete path
|
||||
await new Promise(r => setTimeout(r, 700));
|
||||
assert.deepStrictEqual(calls, []); // the queued save never fired
|
||||
assert.strictEqual(_testScripts.length, 1);
|
||||
assert.strictEqual(_testScripts[0].name, 'New test script');
|
||||
assert.deepStrictEqual(calls, ['/api/test/scripts/delete']);
|
||||
assert.strictEqual(script._deleted, true);
|
||||
});
|
||||
|
||||
test('the STATUS step leaves the SW check to the server default', () => {
|
||||
@@ -606,12 +619,16 @@ test('the run sends the stored preset id, not the removed flat fields', () => {
|
||||
test('the run refuses without a matched or complete preset', () => {
|
||||
globalThis.cards = [];
|
||||
globalThis.cardsMatchedPreset = () => null;
|
||||
assert.match(testRunPreset().error, /No card preset matches/);
|
||||
assert.match(testRunPreset(true).error, /No card preset matches/);
|
||||
const preset = { id: 'p2' };
|
||||
globalThis.cards = [preset];
|
||||
globalThis.cardsMatchedPreset = () => preset;
|
||||
globalThis.cardsScp80Complete = () => false;
|
||||
assert.match(testRunPreset().error, /incomplete SCP80/);
|
||||
assert.match(testRunPreset(true).error, /incomplete SCP80/);
|
||||
// an ADM-only run needs the preset but not the SCP80 completeness
|
||||
assert.strictEqual(testRunPreset(false).preset_id, 'p2');
|
||||
globalThis.cardsScp80Complete = () => true;
|
||||
assert.strictEqual(testRunPreset(true).preset_id, 'p2');
|
||||
});
|
||||
|
||||
test('the client-side problem check bounds the keyset number', () => {
|
||||
@@ -681,10 +698,11 @@ test('a new script is created in the server store and gets its id', async () =>
|
||||
assert.strictEqual(calls[0][0], '/api/test/scripts');
|
||||
assert.strictEqual(calls[0][1].name, 'new');
|
||||
assert.strictEqual(s.id, 'a'.repeat(32)); // the store id is adopted
|
||||
assert.strictEqual(calls[1][0], '/api/test/suites'); // the member refetch
|
||||
// every later write updates by id
|
||||
await testScriptsWriteback(s);
|
||||
assert.strictEqual(calls[1][0], '/api/test/scripts/update');
|
||||
assert.strictEqual(calls[1][1].id, 'a'.repeat(32));
|
||||
assert.strictEqual(calls[2][0], '/api/test/scripts/update');
|
||||
assert.strictEqual(calls[2][1].id, 'a'.repeat(32));
|
||||
});
|
||||
|
||||
test('an incomplete draft is not written to the store', async () => {
|
||||
@@ -701,11 +719,11 @@ test('an immediate save writes the current script through', async () => {
|
||||
globalThis.pysimFetch = async (path) => { calls.push(path); return {ok: true, script: {id: 'c'.repeat(32)}}; };
|
||||
globalThis.ioStatus = () => {};
|
||||
_testScripts = [{name: 'one', steps: [{type: 'action', kind: 'status', params: {attempts: 1}}]}];
|
||||
_testCurrentIdx = 0;
|
||||
globalThis.testCurrent = () => _testScripts[_testCurrentIdx];
|
||||
_testScriptId = null;
|
||||
globalThis.testCurrentScript = () => _testScripts[0];
|
||||
testScriptsSave(true);
|
||||
await new Promise(r => setTimeout(r, 0)); // the writeback is async
|
||||
assert.deepStrictEqual(calls, ['/api/test/scripts']);
|
||||
assert.deepStrictEqual(calls, ['/api/test/scripts', '/api/test/suites']);
|
||||
assert.strictEqual(_testScripts[0].id, 'c'.repeat(32));
|
||||
});
|
||||
|
||||
@@ -723,7 +741,7 @@ test('the run sends the stored script id with the edited script', async () => {
|
||||
sent = {path, body};
|
||||
return {running: true, name: 'saved', index: 0, total: 1, steps: [], status: null};
|
||||
};
|
||||
globalThis.testCurrent = () => ({id: 'b'.repeat(32), name: 'saved',
|
||||
globalThis.testCurrentScript = () => ({id: 'b'.repeat(32), name: 'saved',
|
||||
steps: [{type: 'action', kind: 'status', params: {attempts: 1}}]});
|
||||
globalThis.testRunPreset = () => { throw new Error('no preset for a status-only script'); };
|
||||
globalThis.testRunMessage = () => {};
|
||||
|
||||
Reference in New Issue
Block a user