import assert from "node:assert/strict"; import test from "node:test"; class FakeClassList { constructor() { this.values = new Set(); } toggle(name, enabled) { if (enabled) { this.values.add(name); } else { this.values.delete(name); } } } class FakeElement { constructor(tagName) { this.tagName = tagName; this.children = []; this.listeners = {}; this.classList = new FakeClassList(); this.className = ""; this.textContent = ""; this.value = ""; this.disabled = false; } setAttribute(name, value) { this[name] = value; } append(...children) { this.children.push(...children); } addEventListener(type, listener) { this.listeners[type] = listener; } } function installDom() { const styles = new Map(); globalThis.document = { head: { append(element) { if (element.id) styles.set(element.id, element); }, }, getElementById(id) { return styles.get(id) ?? null; }, createElement(tagName) { return new FakeElement(tagName); }, }; globalThis.window = { confirm: () => true, prompt: () => "", }; globalThis.setTimeout = () => 0; } installDom(); const {statusControl} = await import("../src/components/status-control.js"); function okResponse(overrides = {}) { return { target_type: "task", target_id: "00000000-0000-0000-0000-000000000001", actor: "dashboard", current_status: "todo", target_status: "progress", file_backed: true, archived_file: false, task_linked: true, reconciliation_class: "write_through", reason: "task status can be represented in the workplan task block", follow_up: "patch task block status and sync the DB from file", write_through_result: "applied", workplan_path: "workplans/STATE-WP-9999-demo.md", reconciliation_record_id: null, conflict: false, ...overrides, }; } test("status control posts dashboard changes through reconciliation", async () => { const requests = []; globalThis.fetch = async (url, options) => { requests.push({url, options, body: JSON.parse(options.body)}); return { ok: true, json: async () => okResponse(), }; }; const entity = {id: "00000000-0000-0000-0000-000000000001", status: "todo"}; let saved = null; const root = statusControl({ entity, type: "task", statuses: ["todo", "progress"], onSaved: (updated, result) => { saved = {updated, result}; }, }); const [select, message] = root.children; select.value = "progress"; await select.listeners.change(); assert.equal(requests.length, 1); assert.equal(requests[0].url, "http://127.0.0.1:8000/reconciliation/state-change"); assert.equal(requests[0].body.target_type, "task"); assert.equal(requests[0].body.target_status, "progress"); assert.equal(requests[0].body.expected_current_status, "todo"); assert.equal(requests[0].body.apply, true); assert.equal(entity.status, "progress"); assert.equal(message.textContent, "synced"); assert.equal(saved.result.write_through_result, "applied"); }); test("status control keeps local state on reconciliation conflicts", async () => { const requests = []; globalThis.fetch = async (url, options) => { requests.push({url, options, body: JSON.parse(options.body)}); return { ok: true, json: async () => okResponse({ current_status: "done", target_status: "progress", reconciliation_class: "deferred", reason: "cached task status changed from expected 'todo' to 'done'", follow_up: "refresh the dashboard and retry the state change if it is still intended", write_through_result: "not_applicable", reconciliation_record_id: "00000000-0000-0000-0000-000000000002", conflict: true, }), }; }; const entity = {id: "00000000-0000-0000-0000-000000000001", status: "todo"}; let saved = null; const root = statusControl({ entity, type: "task", statuses: ["todo", "progress"], onSaved: () => { saved = true; }, }); const [select, message] = root.children; select.value = "progress"; await select.listeners.change(); assert.equal(requests.length, 1); assert.equal(entity.status, "todo"); assert.equal(select.value, "todo"); assert.equal(message.textContent, "out of sync"); assert.equal(saved, null); });