Advance M7 workflows and release operations
This commit is contained in:
68
web/tests/unit/project-action-mutex.test.mjs
Normal file
68
web/tests/unit/project-action-mutex.test.mjs
Normal file
@@ -0,0 +1,68 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import ts from "typescript";
|
||||
|
||||
const repoRoot = path.resolve(import.meta.dirname, "../../..");
|
||||
const sourcePath = path.join(repoRoot, "web/protocol/project-action-mutex.ts");
|
||||
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
|
||||
fileName: sourcePath,
|
||||
reportDiagnostics: true,
|
||||
});
|
||||
assert.deepEqual(transpiled.diagnostics, []);
|
||||
const moduleUrl = `data:text/javascript;base64,${Buffer.from(transpiled.outputText).toString("base64")}`;
|
||||
const {
|
||||
PROJECT_ACTION_CONFLICT_MATRIX,
|
||||
acquireProjectAction,
|
||||
createProjectActionMutexState,
|
||||
releaseProjectAction,
|
||||
} = await import(moduleUrl);
|
||||
|
||||
const action = (kind, sequence) => ({ kind, actionId: `${kind}:${sequence}` });
|
||||
|
||||
test("M7-02 keeps the project action conflict matrix symmetric and exclusive", () => {
|
||||
for (const requested of ["OPEN", "SAVE", "CLOSE"]) {
|
||||
for (const owner of ["OPEN", "SAVE", "CLOSE"]) {
|
||||
assert.equal(PROJECT_ACTION_CONFLICT_MATRIX[requested][owner], true);
|
||||
assert.equal(PROJECT_ACTION_CONFLICT_MATRIX[requested][owner], PROJECT_ACTION_CONFLICT_MATRIX[owner][requested]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("M7-02 rejects repeated open with its original owner unchanged", () => {
|
||||
const first = action("OPEN", 1);
|
||||
const second = action("OPEN", 2);
|
||||
const acquired = acquireProjectAction(createProjectActionMutexState(), first);
|
||||
assert.equal(acquired.granted, true);
|
||||
assert.deepEqual(acquireProjectAction(acquired.state, second), {
|
||||
granted: false,
|
||||
state: acquired.state,
|
||||
conflict: { code: "USER_ACTION_CONFLICT", reason: "REPEATED_OPEN", requested: second, owner: first },
|
||||
});
|
||||
});
|
||||
|
||||
test("M7-02 rejects concurrent save and close-during-save with stable reasons", () => {
|
||||
const first = action("SAVE", 1);
|
||||
const acquired = acquireProjectAction(createProjectActionMutexState(), first);
|
||||
assert.equal(acquired.granted, true);
|
||||
assert.equal(acquireProjectAction(acquired.state, action("SAVE", 2)).conflict.reason, "CONCURRENT_SAVE");
|
||||
assert.equal(acquireProjectAction(acquired.state, action("CLOSE", 3)).conflict.reason, "CLOSE_DURING_SAVE");
|
||||
});
|
||||
|
||||
test("M7-02 only lets the exact owner release the project lock", () => {
|
||||
const owner = action("SAVE", 1);
|
||||
const acquired = acquireProjectAction(createProjectActionMutexState(), owner);
|
||||
assert.equal(acquired.granted, true);
|
||||
assert.deepEqual(releaseProjectAction(acquired.state, action("SAVE", 2)), {
|
||||
released: false,
|
||||
state: acquired.state,
|
||||
errorCode: "PROJECT_ACTION_LOCK_IDENTITY_MISMATCH",
|
||||
});
|
||||
assert.deepEqual(releaseProjectAction(acquired.state, owner), {
|
||||
released: true,
|
||||
state: { owner: null },
|
||||
errorCode: null,
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user