joule-moq-web: a turn a later schedule replaces ends at that schedule's from #620
4 changed files with 113 additions and 8 deletions
|
|
@ -1755,7 +1755,18 @@ async function scenarioCo(run) {
|
|||
const floor = onHost.floor || [];
|
||||
const out = floor.filter((f) => f.floor === "out_of_turn");
|
||||
const lateF = floor.filter((f) => f.late);
|
||||
check(`${label}: the room with the clock's log verifies (co-performance §9): accept`, onHost.outcome === "accept", onHost.outcome === "accept" ? `${objects.length} objects, ${floor.length} placed` : `${onHost.reason} at ${JSON.stringify(onHost.at)}`);
|
||||
// A refusal at an object names it, its arrival and the turn boundary or schedule `from`
|
||||
// nearest it: one a hop past an edge is the clock's arrival trailing the relay's (§8, §12).
|
||||
const refusedAt = async (r) => {
|
||||
const o = Array.isArray(r.at) && r.at[0] === "object" ? objects[r.at[1]] : null;
|
||||
if (!o) return `${r.reason} at ${JSON.stringify(r.at)}`;
|
||||
const ss = (await co(V[0].p)).schedules;
|
||||
const edges = ss.flatMap((s) => [s.from, ...s.segments.flatMap((g) => Array.from({ length: g.turns + 1 }, (_, i) => g.start + i * g.turn_us))]);
|
||||
const e = edges.reduce((b, x) => (Math.abs(o.arrival - x) < Math.abs(o.arrival - b) ? x : b), edges[0]);
|
||||
const who = o.principal === A.principal ? "the lead" : o.principal === B.principal ? "the AI participant" : o.principal.slice(0, 12);
|
||||
return `${r.reason} at ${JSON.stringify(r.at)}: ${who}'s object, arrived ${o.arrival} µs, ${o.arrival - e} µs from the edge at ${e}; the schedules from ${ss.map((s) => `${s.from} (${s.mode})`).join(", ")}`;
|
||||
};
|
||||
check(`${label}: the room with the clock's log verifies (co-performance §9): accept`, onHost.outcome === "accept", onHost.outcome === "accept" ? `${objects.length} objects, ${floor.length} placed` : await refusedAt(onHost));
|
||||
check(`${label}: the page's verification of the room is the host's`, JSON.stringify(inPage) === JSON.stringify(onHost), inPage.outcome);
|
||||
check(`${label}: every object out of turn and every late object is marked`, out.length > 0 && out.every((f) => f.marks & 1) && lateF.length > 0 && lateF.every((f) => f.marks & 2), `${out.length} out of turn, ${lateF.length} late`);
|
||||
const intents = (onHost.intents || []).map((i) => i.outcome);
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ fi
|
|||
BROWSER_BIN=${BROWSER_BIN:-}
|
||||
|
||||
step "unit tests of the page's modules"
|
||||
node --test "$CRATE/e2e/compose.test.mjs" "$CRATE/e2e/remote_input.test.mjs" "$CRATE/web/moq/capsule.test.mjs" "$CRATE/web/moq/media.test.mjs" "$CRATE/web/moq/session.test.mjs" "$CRATE/web/ads/presentation.test.mjs" "$CRATE/web/room/privacy-pass.test.mjs" "$CRATE/web/room/binding.test.mjs" "$CRATE/web/room/cowatch.test.mjs" "$CRATE/web/room/remote.test.mjs" "$CRATE/web/room/markup.test.mjs" "$CRATE/web/room/endpoint.test.mjs" "$CRATE/web/room/termination.test.mjs"
|
||||
node --test "$CRATE/e2e/compose.test.mjs" "$CRATE/e2e/remote_input.test.mjs" "$CRATE/web/moq/capsule.test.mjs" "$CRATE/web/moq/media.test.mjs" "$CRATE/web/moq/session.test.mjs" "$CRATE/web/ads/presentation.test.mjs" "$CRATE/web/room/privacy-pass.test.mjs" "$CRATE/web/room/binding.test.mjs" "$CRATE/web/room/cowatch.test.mjs" "$CRATE/web/room/co.test.mjs" "$CRATE/web/room/remote.test.mjs" "$CRATE/web/room/markup.test.mjs" "$CRATE/web/room/endpoint.test.mjs" "$CRATE/web/room/termination.test.mjs"
|
||||
|
||||
step "end-to-end"
|
||||
browser=()
|
||||
|
|
|
|||
|
|
@ -89,15 +89,28 @@ export class Co {
|
|||
|
||||
// ------------------------------------------------------------------------------ the floor
|
||||
|
||||
scheduleAt(x) {
|
||||
for (let i = this.schedules.length - 1; i >= 0; i--) if (this.schedules[i].from <= x) return this.schedules[i];
|
||||
return null;
|
||||
// The schedule in effect at `x` (§6.1): of those whose `from` is at or below it, the last
|
||||
// stated; its index in the room's order, or -1.
|
||||
scheduleIndexAt(x) {
|
||||
for (let i = this.schedules.length - 1; i >= 0; i--) if (this.schedules[i].from <= x) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// The turn running at room-clock `x` (§6.2), with its holder at `x` (§6.3).
|
||||
scheduleAt(x) {
|
||||
const k = this.scheduleIndexAt(x);
|
||||
return k < 0 ? null : this.schedules[k];
|
||||
}
|
||||
|
||||
// The turn running at room-clock `x` (§6.2), with its holder at `x` (§6.3). A turn ends at its
|
||||
// segment's next boundary, or at the `from` of a later schedule, which replaces its own from
|
||||
// then on (§6.1), if that comes first: the guard a performer keeps before a turn's end (§8)
|
||||
// keeps it before either, or its last objects reach the relay under one schedule and the room
|
||||
// clock, a hop later, under the next.
|
||||
turnAt(x) {
|
||||
const s = this.scheduleAt(x);
|
||||
if (!s) return null;
|
||||
const k = this.scheduleIndexAt(x);
|
||||
if (k < 0) return null;
|
||||
const s = this.schedules[k];
|
||||
const replaced = this.schedules.slice(k + 1).reduce((m, r) => Math.min(m, r.from), Infinity);
|
||||
for (let i = 0; i < s.segments.length; i++) {
|
||||
const g = s.segments[i];
|
||||
const L = g.turn_us;
|
||||
|
|
@ -106,7 +119,7 @@ export class Co {
|
|||
const n = Math.floor((x - g.start) / L);
|
||||
const scheduled = g.holders[n % g.holders.length];
|
||||
const h = this.handovers.filter((h) => h.schedule === s.digest && h.segment === i && h.turn === n && h.at <= x).sort((a, b) => a.at - b.at || a.order - b.order).pop();
|
||||
return { schedule: s, segment: i, index: n, start: g.start + n * L, end: g.start + (n + 1) * L, scheduled, holder: h ? h.to : scheduled, holders: g.holders, role: g.pairs ? (n % 2 ? "response" : "call") : "solo", mode: s.mode, of: g.turns };
|
||||
return { schedule: s, segment: i, index: n, start: g.start + n * L, end: Math.min(g.start + (n + 1) * L, replaced), scheduled, holder: h ? h.to : scheduled, holders: g.holders, role: g.pairs ? (n % 2 ? "response" : "call") : "solo", mode: s.mode, of: g.turns };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
// The room page's co-performance floor (co.js, `node --test web/room/co.test.mjs`): the turn
|
||||
// running at an instant ends at its segment's boundary, or at the `from` of a later schedule
|
||||
// that replaces its own (co-performance §6.1), whichever comes first; and a performer's page
|
||||
// keeps its guard (§8) before either, so that its last objects reach the relay and the room's
|
||||
// clock, a hop later, under the same schedule.
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { Co, GUARD_US } from "./co.js";
|
||||
|
||||
const A = "aa".repeat(32);
|
||||
const B = "bb".repeat(32);
|
||||
|
||||
/** A page of `principal`, holding `schedules` in the room's order, with no records else. */
|
||||
function page(principal, schedules) {
|
||||
const co = new Co({ wai: { coPolicy: () => "{}" }, me: () => ({ principal }), policyBytes: null });
|
||||
co.schedules = schedules.map((s, i) => ({ digest: `s${i}`, order: i + 1, ...s }));
|
||||
return co;
|
||||
}
|
||||
|
||||
// Six turns of 5 s from 0, A then B, under mark; then, from 7 s, two turns of 4 s, A then B,
|
||||
// under refuse. B holds the first schedule's second turn, [5 s, 10 s), when the second takes
|
||||
// effect.
|
||||
const MARK = { mode: "mark", from: 0, segments: [{ start: 0, turn_us: 5_000_000, turns: 6, pairs: 0, holders: [A, B] }] };
|
||||
const REFUSE = { mode: "refuse", from: 7_000_000, segments: [{ start: 7_000_000, turn_us: 4_000_000, turns: 2, pairs: 0, holders: [A, B] }] };
|
||||
|
||||
test("a turn a later schedule replaces ends at that schedule's from", () => {
|
||||
const co = page(B, [MARK, REFUSE]);
|
||||
const t = co.turnAt(6_900_000);
|
||||
assert.equal(t.holder, B);
|
||||
assert.equal(t.start, 5_000_000);
|
||||
assert.equal(t.end, 7_000_000);
|
||||
assert.equal(t.mode, "mark");
|
||||
const u = co.turnAt(7_000_000);
|
||||
assert.equal(u.holder, A);
|
||||
assert.equal(u.mode, "refuse");
|
||||
assert.equal(u.start, 7_000_000);
|
||||
assert.equal(u.end, 11_000_000);
|
||||
});
|
||||
|
||||
test("a turn no later schedule replaces ends at its segment's boundary", () => {
|
||||
const co = page(B, [MARK]);
|
||||
assert.equal(co.turnAt(6_900_000).end, 10_000_000);
|
||||
assert.equal(co.turnAt(1_000_000).end, 5_000_000);
|
||||
});
|
||||
|
||||
test("the holder of a replaced turn stops its guard before the next schedule's from", () => {
|
||||
const co = page(B, [MARK, REFUSE]);
|
||||
assert.equal(co.sends(7_000_000 - GUARD_US - 1), true);
|
||||
assert.equal(co.sends(7_000_000 - GUARD_US), false);
|
||||
assert.equal(co.sends(6_999_999), false);
|
||||
// From the from on, B does not hold the floor until its turn of the new schedule, guarded.
|
||||
assert.equal(co.sends(9_000_000), false);
|
||||
assert.equal(co.sends(11_000_000 + GUARD_US - 1), false);
|
||||
assert.equal(co.sends(11_000_000 + GUARD_US), true);
|
||||
});
|
||||
|
||||
test("the holder of the next schedule's first turn starts its guard after the from", () => {
|
||||
const co = page(A, [MARK, REFUSE]);
|
||||
assert.equal(co.sends(7_000_000 + GUARD_US - 1), false);
|
||||
assert.equal(co.sends(7_000_000 + GUARD_US), true);
|
||||
});
|
||||
|
||||
test("the turns ahead step from the replaced turn to the next schedule's", () => {
|
||||
const co = page(B, [MARK, REFUSE]);
|
||||
const ahead = co.turnsAhead(6_000_000, 3).map((t) => [t.holder, t.start, t.end, t.mode]);
|
||||
assert.deepEqual(ahead, [
|
||||
[B, 5_000_000, 7_000_000, "mark"],
|
||||
[A, 7_000_000, 11_000_000, "refuse"],
|
||||
[B, 11_000_000, 15_000_000, "refuse"],
|
||||
]);
|
||||
});
|
||||
|
||||
test("a schedule stated later governs even where an earlier-stated one's from is later", () => {
|
||||
// §6.1: the schedule in effect is the last stated of those whose from has come. REFUSE was
|
||||
// stated first; MARK, stated after it, governs from 0 on and nothing replaces it.
|
||||
const co = page(B, [REFUSE, MARK]);
|
||||
const t = co.turnAt(6_900_000);
|
||||
assert.equal(t.mode, "mark");
|
||||
assert.equal(t.end, 10_000_000);
|
||||
assert.equal(co.turnAt(8_000_000).mode, "mark");
|
||||
});
|
||||
Loading…
Reference in a new issue