test/context-view-preferences.test.js
Rama main · réplica de lectura del repositorio original.
var assert = require("assert");
var test = require("node:test");
var fs = require("fs");
var path = require("path");
var vm = require("vm");
var storageModule = require("../lib/users-context-view-preferences");
var serviceModule = require("../lib/server-context-view");
function storage(initial, multiUser, configInitial) {
var data = JSON.parse(JSON.stringify(initial));
var config = Object.assign({}, configInitial || {});
var configModule = { loadConfig: function () { return Object.assign({}, config); }, saveConfig: function (next) { config = Object.assign({}, next); } };
var preferences = storageModule.attachContextViewPreferences({
loadUsers: function () { return JSON.parse(JSON.stringify(data)); },
saveUsers: function (next) { data = JSON.parse(JSON.stringify(next)); },
isMultiUser: function () { return multiUser; },
configModule: configModule,
});
return { preferences: preferences, data: function () { return data; }, config: function () { return config; } };
}
test("context view persists across fresh single-user instances while preserving config", function () {
var fixture = storage({ users: [] }, false, { port: 2633, tls: true });
assert.deepEqual(fixture.preferences.get("default"), { present: false, mode: "off", source: null });
assert.equal(fixture.preferences.set("default", "panel").ok, true);
var reloaded = storage(fixture.data(), false, fixture.config());
assert.equal(reloaded.preferences.get("default").mode, "panel");
assert.equal(reloaded.config().port, 2633);
assert.equal(reloaded.preferences.set("default", "invalid").ok, false);
});
test("context view isolates multi-user records and rejects missing users", function () {
var fixture = storage({ users: [{ id: "u1" }, { id: "u2", contextViewPreference: "mini" }] }, true);
assert.equal(fixture.preferences.set("u1", "panel").ok, true);
assert.equal(fixture.preferences.get("u1").mode, "panel");
assert.equal(fixture.preferences.get("u2").mode, "mini");
assert.equal(fixture.preferences.set("missing", "off").ok, false);
});
function serviceFixture(options) {
options = options || {};
var records = { u1: { id: "u1" }, u2: { id: "u2" } };
var persisted = storage({ users: [{ id: "u1" }, { id: "u2" }] }, true);
var clients = [];
var users = {
isMultiUser: function () { return true; },
findUserById: function (id) { return records[id] || null; },
getContextViewPreference: function (id) { return persisted.preferences.get(id); },
setContextViewPreference: function (id, mode) { return persisted.preferences.set(id, mode); },
};
var service = serviceModule.attachContextViewService({ users: users, forEachAppClient: function (fn) { clients.forEach(fn); }, serverEpoch: "context-test" });
function ws(id) { var socket = { readyState: 1, _clayUser: { id: id }, sent: [] }; socket.send = function (message) { socket.sent.push(JSON.parse(message)); }; return socket; }
return { service: service, ws: ws, clients: clients, records: records };
}
test("context view broadcasts across projects only to the same user", function () {
var fixture = serviceFixture();
var sourceProject = fixture.ws("u1");
var otherProject = fixture.ws("u1");
var otherUser = fixture.ws("u2");
fixture.clients.push(otherProject, otherUser);
fixture.service.handleMessage(sourceProject, { type: "context_view_set", requestId: "save", mode: "mini" });
assert.equal(otherProject.sent[0].mode, "mini");
assert.equal(otherUser.sent.length, 0);
assert.equal(sourceProject.sent[0].requestId, "save");
});
test("context view service rejects invalid modes and revoked actors using real storage", function () {
var fixture = serviceFixture();
var invalid = fixture.ws("u1");
fixture.service.handleMessage(invalid, { type: "context_view_set", requestId: "invalid", mode: "not-a-mode" });
assert.equal(invalid.sent[0].ready, false);
assert.match(invalid.sent[0].error, /invalid/i);
var revoked = fixture.ws("u1");
fixture.records.u1 = null;
fixture.service.handleMessage(revoked, { type: "context_view_get", requestId: "revoked" });
assert.equal(revoked.sent[0].accountAvailable, false);
assert.equal(revoked.sent[0].accountId, null);
});
function loadClient() {
var source = fs.readFileSync(path.join(__dirname, "../lib/public/modules/context-view-preference.js"), "utf8");
source = source.replace(/^import .*;\n/gm, "").replace(/export function /g, "function ").replace(/export \{ validMode \};/g, "");
source += "\nthis.api={getContextView:getContextView,getEffectiveContextView:getEffectiveContextView,beginContextViewConnection:beginContextViewConnection,requestContextView:requestContextView,setContextView:setContextView,handleContextViewMessage:handleContextViewMessage};";
var state = { connected: true, myUserId: "u1", currentSlug: "project-a", contextViewOverride: null, contextViewPreferenceState: { mode: "off", canonicalMode: "off", preferencePresent: false, loading: false, saving: false, requestId: null, saveRequestId: null, pendingSaves: [], accountId: "u1", serverEpoch: "old", canonicalRevision: 2, error: "" } };
var listeners = [];
var sent = [];
var context = {
store: { get: function (key) { return state[key]; }, set: function (value) { var previous = state; state = Object.assign({}, state, value); listeners.forEach(function (listener) { listener(state, previous); }); }, subscribe: function (listener) { listeners.push(listener); } },
getWs: function () { return { readyState: 1, send: function (message) { sent.push(JSON.parse(message)); } }; },
Date: Date,
};
vm.runInNewContext(source, context);
return { api: context.api, state: function () { return state; }, sent: sent, set: function (value) { context.store.set(value); } };
}
test("context view client confirms older rapid save before rolling back latest failure", function () {
var fixture = loadClient();
fixture.api.setContextView("mini");
var first = fixture.state().contextViewPreferenceState.saveRequestId;
fixture.api.setContextView("panel");
var second = fixture.state().contextViewPreferenceState.saveRequestId;
fixture.api.handleContextViewMessage({ type: "context_view_state", requestId: first, accountId: "u1", serverEpoch: "old", canonicalRevision: 3, mode: "mini", ready: true });
assert.equal(fixture.state().contextViewPreferenceState.mode, "panel");
assert.equal(fixture.state().contextViewPreferenceState.saveRequestId, second);
fixture.api.handleContextViewMessage({ type: "context_view_state", requestId: second, accountId: "u1", serverEpoch: "old", canonicalRevision: 4, ready: false, error: "save failed" });
assert.equal(fixture.state().contextViewPreferenceState.saving, false);
assert.equal(fixture.api.getContextView(), "mini");
assert.equal(fixture.state().contextViewPreferenceState.error, "save failed");
});
test("context view broadcast updates canonical mode without replacing pending optimistic mode", function () {
var fixture = loadClient();
fixture.api.setContextView("mini");
var saveId = fixture.state().contextViewPreferenceState.saveRequestId;
fixture.api.handleContextViewMessage({ type: "context_view_state", accountId: "u1", serverEpoch: "old", canonicalRevision: 3, mode: "panel", preferencePresent: true });
assert.equal(fixture.api.getContextView(), "mini");
assert.equal(fixture.state().contextViewPreferenceState.canonicalMode, "panel");
assert.equal(fixture.state().contextViewPreferenceState.saveRequestId, saveId);
fixture.api.handleContextViewMessage({ type: "context_view_state", requestId: saveId, accountId: "u1", serverEpoch: "old", canonicalRevision: 4, mode: "mini", ready: true });
assert.equal(fixture.api.getContextView(), "mini");
assert.equal(fixture.state().contextViewPreferenceState.saving, false);
});
test("context view reverse-order ACK keeps newer confirmed intent and ignores older revision", function () {
var fixture = loadClient();
fixture.api.setContextView("mini");
var first = fixture.state().contextViewPreferenceState.saveRequestId;
fixture.api.setContextView("panel");
var second = fixture.state().contextViewPreferenceState.saveRequestId;
fixture.api.handleContextViewMessage({ type: "context_view_state", requestId: second, accountId: "u1", serverEpoch: "old", canonicalRevision: 4, mode: "panel", ready: true, preferencePresent: true });
assert.equal(fixture.api.getContextView(), "panel");
assert.equal(fixture.state().contextViewPreferenceState.canonicalMode, "panel");
assert.equal(fixture.state().contextViewPreferenceState.canonicalRevision, 4);
assert.equal(fixture.state().contextViewPreferenceState.saving, false);
fixture.api.handleContextViewMessage({ type: "context_view_state", requestId: first, accountId: "u1", serverEpoch: "old", canonicalRevision: 3, mode: "mini", ready: true });
assert.equal(fixture.api.getContextView(), "panel");
assert.equal(fixture.state().contextViewPreferenceState.canonicalMode, "panel");
assert.equal(fixture.state().contextViewPreferenceState.canonicalRevision, 4);
var ordered = loadClient();
ordered.api.setContextView("mini");
var older = ordered.state().contextViewPreferenceState.saveRequestId;
ordered.api.setContextView("panel");
var newer = ordered.state().contextViewPreferenceState.saveRequestId;
ordered.api.handleContextViewMessage({ type: "context_view_state", accountId: "u1", serverEpoch: "old", canonicalRevision: 4, mode: "panel", ready: true, preferencePresent: true });
ordered.api.handleContextViewMessage({ type: "context_view_state", requestId: older, accountId: "u1", serverEpoch: "old", canonicalRevision: 3, mode: "mini", ready: true });
assert.equal(ordered.state().contextViewPreferenceState.canonicalRevision, 4);
assert.equal(ordered.api.getContextView(), "panel");
ordered.api.handleContextViewMessage({ type: "context_view_state", requestId: newer, accountId: "u1", serverEpoch: "old", canonicalRevision: 5, ready: false, error: "save failed" });
assert.equal(ordered.api.getContextView(), "panel");
assert.equal(ordered.state().contextViewPreferenceState.canonicalMode, "panel");
});
test("context view correlated account loss settles the pending save", function () {
var fixture = loadClient();
fixture.api.setContextView("panel");
var saveId = fixture.state().contextViewPreferenceState.saveRequestId;
fixture.api.handleContextViewMessage({ type: "context_view_state", requestId: saveId, accountId: null, accountAvailable: false, serverEpoch: "old", error: "Your account is no longer available." });
assert.equal(fixture.state().contextViewPreferenceState.saving, false);
assert.equal(fixture.state().contextViewPreferenceState.saveRequestId, null);
assert.equal(fixture.api.getContextView(), "off");
});
test("context view reconnect drops lost save before the new GET reply", function () {
var fixture = loadClient();
fixture.api.setContextView("panel");
fixture.api.beginContextViewConnection();
assert.equal(fixture.api.getContextView(), "off");
assert.equal(fixture.state().contextViewPreferenceState.saving, false);
assert.equal(fixture.state().contextViewPreferenceState.saveRequestId, null);
fixture.api.requestContextView();
var requestId = fixture.state().contextViewPreferenceState.requestId;
fixture.api.handleContextViewMessage({ type: "context_view_state", requestId: requestId, accountId: "u1", serverEpoch: "new", canonicalRevision: 0, mode: "mini", preferencePresent: true, ready: true });
assert.equal(fixture.api.getContextView(), "mini");
assert.equal(fixture.state().contextViewPreferenceState.loading, false);
});
test("app panels use server preference authority and pane override remains transient", function () {
var panels = fs.readFileSync(path.join(__dirname, "../lib/public/modules/app-panels.js"), "utf8");
var pane = fs.readFileSync(path.join(__dirname, "../lib/public/modules/pane-bridge.js"), "utf8");
assert.equal(panels.indexOf("localStorage.getItem(\"clay-context-view\")"), -1);
assert.equal(panels.indexOf("localStorage.setItem(\"clay-context-view\")"), -1);
assert.ok(panels.indexOf("getStoredContextView") !== -1);
assert.ok(panels.indexOf("store.subscribe") !== -1);
assert.equal(panels.indexOf("clay-context-view-changed"), -1);
assert.doesNotMatch(panels, /setContextView\("off"\)\) applyContextView/);
assert.doesNotMatch(panels, /setContextView\("mini"\)\) applyContextView/);
assert.doesNotMatch(panels, /setContextView\("panel"\)\) applyContextView/);
assert.match(pane, /contextViewOverride/);
});
test("context view effective mode uses the transient store override without changing preference", function () {
var fixture = loadClient();
fixture.set({ contextViewOverride: "panel" });
fixture.set({ paneMode: false });
assert.equal(fixture.api.getEffectiveContextView(), "off");
fixture.set({ paneMode: true });
assert.equal(fixture.api.getEffectiveContextView(), "panel");
fixture.set({ contextViewPreferenceState: Object.assign({}, fixture.state().contextViewPreferenceState, { mode: "mini", canonicalMode: "mini" }) });
fixture.set({ contextViewOverride: "off" });
assert.equal(fixture.api.getEffectiveContextView(), "off");
fixture.set({ contextViewOverride: null });
assert.equal(fixture.api.getEffectiveContextView(), "mini");
});
test("context view boundary reset clears the pane override", function () {
var fixture = loadClient();
fixture.set({ contextViewOverride: "panel" });
fixture.set({ myUserId: "u2" });
assert.equal(fixture.state().contextViewOverride, null);
assert.equal(fixture.state().contextViewPreferenceState.mode, "off");
});