test/issues-phase1.test.js
Rama main · réplica de lectura del repositorio original.
var assert = require("node:assert/strict");
var fs = require("node:fs");
var os = require("node:os");
var path = require("node:path");
var execFileSync = require("node:child_process").execFileSync;
var test = require("node:test");
var home = fs.mkdtempSync(path.join(os.tmpdir(), "clay-issues-home-"));
process.env.CLAY_HOME = home;
process.env.CLAY_DEV = "";
var temporaryPaths = [home];
function temporary(prefix) {
var result = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
temporaryPaths.push(result);
return result;
}
test.after(function () {
for (var i = 0; i < temporaryPaths.length; i++) fs.rmSync(temporaryPaths[i], { recursive: true, force: true });
});
var issueStore = require("../lib/issues-store");
var issueService = require("../lib/issues-service").attachIssuesService;
var verifier = require("../lib/issues-commit-verification");
var logsStore = require("../lib/project-logs-store");
function repository() {
var root = temporary("clay-issues-repo-");
function git(args) { return execFileSync("git", args, { cwd: root, encoding: "utf8" }).trim(); }
git(["init", "-q"]);
git(["config", "user.email", "issues@example.com"]);
git(["config", "user.name", "Issues Test"]);
fs.writeFileSync(path.join(root, "app.js"), "var value = 1;\n");
git(["add", "app.js"]);
git(["commit", "-qm", "test: initialize issue repository"]);
var sha = git(["rev-parse", "HEAD"]);
return { root: root, sha: sha };
}
function store() {
return issueStore.createIssuesStore({ projectKnowledgeId: "pk_issues", baseDir: temporary("clay-issues-records-") });
}
function author() { return { type: "user", userId: "u1", displayName: "User One" }; }
test("Issues use a separate namespace and preserve revisions across reload", function () {
var dir = temporary("clay-issues-store-");
var first = issueStore.createIssuesStore({ projectKnowledgeId: "pk_issues", baseDir: dir });
var created = first.create({ title: "Crash", summary: "Fails on launch", body: "Details", type: "bug" }, author());
assert.match(first.scopeId, /^project\/pk_issues\/issues$/);
assert.match(created.ref, /^issue:[A-Za-z0-9_-]{24}$/);
assert.equal(created.revision, 1);
assert.equal(first.list({}).issues[0].body, undefined);
var updated = first.update(created.ref, { status: "in_progress", summary: "Investigating" }, 1, author());
assert.equal(updated.revision, 2);
var reopened = issueStore.createIssuesStore({ projectKnowledgeId: "pk_issues", baseDir: dir });
assert.equal(reopened.read(created.ref).revision, 2);
assert.equal(reopened.history(created.ref).revisions.length, 2);
assert.equal(reopened.readRevision(created.ref, 1).summary, "Fails on launch");
});
test("Issues and Project Logs use isolated append-only namespaces", function () {
var baseDir = temporary("clay-issues-isolated-");
var issues = issueStore.createIssuesStore({ projectKnowledgeId: "pk_shared", baseDir: baseDir });
var logs = logsStore.createProjectLogsStore({ root: "/srv/shared", projectKnowledgeId: "pk_shared", baseDir: baseDir });
issues.create({ title: "Issue record", summary: "Only an issue" }, author());
logs.create({ title: "Log record", summary: "Only a log", kind: "progress" }, author());
assert.equal(issues.scopeId, "project/pk_shared/issues");
assert.equal(logs.scopeId, "project/pk_shared");
assert.notEqual(issues.filePath, logs.filePath);
assert.equal(issues.list({}).total, 1);
assert.equal(logs.list({}).total, 1);
});
test("Issues reject stale revisions and support bounded filters", function () {
var current = store();
var issue = current.create({ title: "Feature", summary: "Add it", type: "feature", priority: "important" }, author());
assert.throws(function () { current.update(issue.ref, { summary: "stale" }, 0, author()); }, /conflict/);
assert.equal(current.read(issue.ref).summary, "Add it");
current.create({ title: "Plan", summary: "Plan it", type: "plan", status: "open" }, author());
assert.equal(current.list({ type: "feature" }).total, 1);
assert.equal(current.list({ status: "open", limit: 1 }).issues.length, 1);
assert.throws(function () { current.list({ type: "nope" }); }, /Invalid issue type/);
assert.throws(function () { current.create({ title: "Null status", status: null }, author()); }, /Invalid status/);
});
test("Issue incorporation validates before one append and replays its review", function () {
var directory = temporary("clay-issues-incorporate-");
var current = issueStore.createIssuesStore({ projectKnowledgeId: "pk_issues", baseDir: directory });
var entry = current.create({ title: "Before", summary: "Context" }, author());
var comment = current.comment(entry.ref, "Please improve this.", { type: "user", userId: "u2", displayName: "Reviewer" });
var commentId = comment.comments[0].id;
var beforeRecords = current.records.all().length;
assert.throws(function () {
current.incorporate(entry.ref, commentId, { title: "After" }, 1, "x".repeat(4001), author());
}, /response exceeds 4000/);
assert.equal(current.read(entry.ref).revision, 1);
assert.equal(current.read(entry.ref).comments[0].status, "pending");
assert.equal(current.records.all().length, beforeRecords);
var incorporated = current.incorporate(entry.ref, commentId, { title: "After" }, 1, "Accepted.", author());
assert.equal(incorporated.revision, 2);
assert.equal(incorporated.title, "After");
assert.equal(incorporated.comments[0].status, "incorporated");
assert.equal(current.records.all().length, beforeRecords + 1);
var secondComment = current.comment(entry.ref, "No actual change.", { type: "user", userId: "u2", displayName: "Reviewer" });
assert.throws(function () { current.incorporate(entry.ref, secondComment.comments[1].id, { title: "After" }, 2, "No-op", author()); }, /must change/);
var reopened = issueStore.createIssuesStore({ projectKnowledgeId: "pk_issues", baseDir: directory });
assert.equal(reopened.read(entry.ref).title, "After");
assert.equal(reopened.read(entry.ref).comments[0].status, "incorporated");
});
test("resolution requires verified commit evidence and reopen retains history", async function () {
var repo = repository();
var current = store();
var evidence = await verifier.verifyCommit(repo.root, repo.sha, "pk_issues");
var issue = current.create({ title: "Fix", summary: "Fixed", resolutionSummary: "Fixed and verified", status: "resolved" }, author(), null, evidence);
assert.equal(issue.verifiedCommit.sha, repo.sha);
var reopened = current.update(issue.ref, { status: "open" }, 1, author());
assert.equal(reopened.status, "open");
assert.equal(reopened.verifiedCommit, undefined);
assert.equal(reopened.resolutionHistory.length, 1);
assert.throws(function () { current.create({ title: "Bad", summary: "No proof", status: "resolved" }, author()); }, /verified commit/);
var closed = current.update(issue.ref, { status: "closed", closeReason: "duplicate" }, 2, author());
assert.equal(closed.closeReason, "duplicate");
});
test("context is immutable, body is searchable, and history reports exact changed fields", function () {
var current = store();
var context = { kind: "worktree", changeSetId: "cs-1", branch: "feature/issues" };
var issue = current.create({ title: "Search", summary: "Short", body: "needle only in analysis" }, author(), context);
var updated = current.update(issue.ref, { context: null, body: "revised body" }, 1, author());
assert.deepEqual(updated.context, context);
assert.equal(current.search({ query: "revised body" }).total, 1);
assert.deepEqual(current.history(issue.ref).revisions[1].changed, ["body"]);
});
test("every resolution stores fresh full commit evidence and reopen clears only current evidence", async function () {
var repo = repository();
var current = store();
var proof = await verifier.verifyCommit(repo.root, repo.sha, "pk_issues");
var issue = current.create({ title: "Repeat", summary: "Tracking", resolutionSummary: "First fix", status: "resolved" }, author(), null, proof);
var open = current.update(issue.ref, { status: "open" }, 1, author());
assert.equal(open.resolutionSummary, "");
assert.equal(open.verifiedCommit, undefined);
assert.equal(open.resolutionHistory.length, 1);
var resolved = current.update(issue.ref, { status: "resolved", resolutionSummary: "Second fix" }, 2, author(), null, proof);
assert.equal(resolved.resolutionHistory.length, 2);
assert.equal(resolved.resolutionHistory[1].verifiedCommit.sha, repo.sha);
assert.throws(function () {
current.update(issue.ref, { status: "resolved", resolutionSummary: "No new proof" }, 3, author());
}, /verified commit/);
assert.throws(function () {
current.create({ title: "Fallback forbidden", summary: "Must not become resolution", status: "resolved" }, author(), null, proof);
}, /resolutionSummary|summary/);
});
function fixture(options) {
var user = { id: "u1", displayName: "User One" };
var owner = { id: "owner", displayName: "Project Owner" };
var session = Object.assign({ localId: 1, ownerId: "u1", cliSessionId: "cli-1", sessionProvenance: null }, options && options.session || {});
var status = Object.assign({ slug: "app", path: "/tmp/app", projectKnowledgeId: "pk_issues", projectOwnerId: "owner", isMate: false, isWorktree: false }, options && options.status || {});
var sessions = new Map([[session.localId, session]]);
var project = { getStatus: function () { return status; }, getSessionManager: function () { return { sessions: sessions }; } };
var projects = new Map([[status.slug, project]]);
var currentUser = user;
var service = issueService({
getProjects: function () { return projects; },
isMultiUser: function () { return true; },
findUserById: function (id) { if (id === owner.id) return owner; return currentUser && id === currentUser.id ? currentUser : null; },
canAccessProject: function (id) { return id === "u1"; },
hasFullProjectAccess: function () { return false; },
baseDir: temporary("clay-issues-service-"),
verifyCommit: function () { return Promise.resolve({ sha: "a".repeat(40), subject: "test: verified", verifiedAt: Date.now(), repository: "pk_issues" }); },
});
return { service: service, session: session, project: project, status: status, users: { user: user, revoke: function () { currentUser = null; } }, projects: projects };
}
test("service revalidates users, sessions, workers, and worktree scope", async function () {
var f = fixture({ status: { projectOwnerId: "u1" } });
var userBound = f.service.bindProjectSession({ projectSlug: "app", session: f.session });
assert.ok(userBound);
var created = await userBound.createIssue({ title: "Driver issue", summary: "Visible" });
assert.equal(userBound.readIssue({ ref: created.ref }).title, "Driver issue");
f.users.revoke();
assert.throws(function () { userBound.listIssues({}); }, /no longer valid/);
var g = fixture({ status: { projectOwnerId: "u1" } });
var bound = g.service.bindProjectSession({ projectSlug: "app", session: g.session });
assert.ok(bound);
g.session.sessionProvenance = { kind: "worker" };
assert.throws(function () { bound.listIssues({}); }, /no longer valid/);
var h = fixture({ status: { projectOwnerId: "owner", isWorktree: true, parentSlug: "app", changeSetId: "cs-1" } });
var worktreeBound = h.service.bindProjectSession({ projectSlug: "app", session: h.session });
assert.ok(worktreeBound, "an authorized exact worktree member may bind");
h.session.sessionProvenance = { kind: "worker" };
assert.throws(function () { worktreeBound.listIssues({}); }, /no longer valid/);
var mate = fixture({ status: { isMate: true } });
assert.equal(mate.service.bindUser({ projectSlug: "app", userId: "u1" }), null);
});
test("commit verification rejects malformed, short, non-commit, and out-of-repository inputs", async function () {
var repo = repository();
await assert.rejects(verifier.verifyCommit(repo.root, "not-a-sha", "pk_issues"), /7 to 64/);
var short = await verifier.verifyCommit(repo.root, repo.sha.substring(0, 7), "pk_issues");
assert.equal(short.sha, repo.sha);
var blob = execFileSync("git", ["hash-object", "-w", "--stdin"], { cwd: repo.root, input: "blob", encoding: "utf8" }).trim();
await assert.rejects(verifier.verifyCommit(repo.root, blob, "pk_issues"), /Command failed/);
var other = repository();
fs.writeFileSync(path.join(other.root, "other.js"), "var other = true;\n");
execFileSync("git", ["add", "other.js"] , { cwd: other.root });
execFileSync("git", ["commit", "-qm", "test: make repository distinct"], { cwd: other.root });
other.sha = execFileSync("git", ["rev-parse", "HEAD"], { cwd: other.root, encoding: "utf8" }).trim();
await assert.rejects(verifier.verifyCommit(repo.root, other.sha, "pk_issues"), /Command failed/);
var valid = await verifier.verifyCommit(repo.root, repo.sha, "pk_issues");
assert.equal(valid.sha, repo.sha);
assert.equal(valid.subject, "test: initialize issue repository");
assert.equal(valid.repository, "pk_issues");
});
test("awaited commit verification rechecks authorization and conflicts before append", async function () {
var gateResolve;
var gate = new Promise(function (resolve) { gateResolve = resolve; });
var f = fixture({ status: { projectOwnerId: "u1" } });
var bound = f.service.bindUser({ projectSlug: "app", userId: "u1" });
var original = bound;
var delayed = issueService({
getProjects: function () { return f.projects; }, isMultiUser: function () { return true; },
findUserById: function () { return f.users.user; }, canAccessProject: function (id) { return id === "u1" && f.status.projectOwnerId === "u1"; },
baseDir: temporary("clay-issues-await-"),
verifyCommit: function () { return gate.then(function () { return { sha: "b".repeat(40), subject: "test: delayed", verifiedAt: Date.now(), repository: "pk_issues" }; }); },
}).bindProjectSession({ projectSlug: "app", session: f.session });
var pending = delayed.createIssue({ title: "Awaited", summary: "Pending", resolutionSummary: "Verified fix", status: "resolved", commitSha: "abcdef0" });
f.status.projectOwnerId = "revoked-owner";
gateResolve();
await assert.rejects(pending, /no longer valid/);
var g = fixture({ status: { projectOwnerId: "u1" } });
var gBound = g.service.bindProjectSession({ projectSlug: "app", session: g.session });
var created = await gBound.createIssue({ title: "Conflict", summary: "Original" });
var first = gBound.updateIssue({ ref: created.ref, expectedRevision: 1, summary: "Changed", status: "in_progress" });
await first;
assert.throws(function () {
gBound.updateIssue({ ref: created.ref, expectedRevision: 1, summary: "Concurrent", status: "in_progress" });
}, /conflict|revision/i);
});
test("parent and restricted worktree bindings share storage but filter before totals and paging", async function () {
var owner = { id: "owner", displayName: "Owner" };
var member = { id: "u1", displayName: "Member" };
var parentStatus = { slug: "app", path: "/tmp/app", projectKnowledgeId: "pk_parent", projectOwnerId: "owner", isMate: false, isWorktree: false };
var firstStatus = { slug: "app-wt-1", path: "/tmp/app-wt-1", projectKnowledgeId: "pk_child_1", projectOwnerId: "owner", isMate: false, isWorktree: true, parentSlug: "app", changeSetId: "cs-1" };
var secondStatus = { slug: "app-wt-2", path: "/tmp/app-wt-2", projectKnowledgeId: "pk_child_2", projectOwnerId: "owner", isMate: false, isWorktree: true, parentSlug: "app", changeSetId: "cs-2" };
var parentSession = { localId: 1, ownerId: "owner" };
var firstSession = { localId: 2, ownerId: "u1" };
var secondSession = { localId: 3, ownerId: "u1" };
function project(status, session) { var sessions = new Map([[session.localId, session]]); return { getStatus: function () { return status; }, getSessionManager: function () { return { sessions: sessions }; } }; }
var projects = new Map([["app", project(parentStatus, parentSession)], ["app-wt-1", project(firstStatus, firstSession)], ["app-wt-2", project(secondStatus, secondSession)]]);
var service = issueService({
getProjects: function () { return projects; }, isMultiUser: function () { return true; },
findUserById: function (id) { return id === "owner" ? owner : id === "u1" ? member : null; },
canAccessProject: function (id) { return id === "u1"; }, hasFullProjectAccess: function () { return false; },
baseDir: temporary("clay-issues-scopes-"),
});
var parent = service.bindProjectSession({ projectSlug: "app", session: parentSession });
var first = service.bindProjectSession({ projectSlug: "app-wt-1", session: firstSession });
var second = service.bindProjectSession({ projectSlug: "app-wt-2", session: secondSession });
assert.equal(parent.scopeId, "project/pk_parent/issues");
assert.equal(first.scopeId, parent.scopeId);
var parentIssue = await parent.createIssue({ title: "Parent", summary: "Project wide" });
var firstIssue = await first.createIssue({ title: "First", summary: "First change set" });
await second.createIssue({ title: "Second", summary: "Second change set" });
assert.equal(parent.listIssues({}).total, 3);
var restricted = first.listIssues({ limit: 1 });
assert.equal(restricted.total, 1);
assert.equal(restricted.issues[0].ref, firstIssue.ref);
assert.equal(restricted.nextCursor, null);
assert.throws(function () { first.readIssue({ ref: parentIssue.ref }); }, /not found/i);
var updated = await first.updateIssue({ ref: firstIssue.ref, expectedRevision: 1, status: "in_progress" });
assert.equal(updated.context.changeSetId, "cs-1");
var missing = Object.assign({}, firstStatus, { slug: "missing", changeSetId: null });
projects.set("missing", project(missing, firstSession));
assert.equal(service.bindUser({ projectSlug: "missing", userId: "u1" }), null);
});
test("public mutations reject forged fields, stale revisions, and invisible refs before Git", async function () {
var calls = 0;
var f = fixture({ status: { projectOwnerId: "u1" } });
var service = issueService({
getProjects: function () { return f.projects; }, isMultiUser: function () { return true; },
findUserById: function (id) { return id === "u1" ? f.users.user : null; },
canAccessProject: function () { return true; }, baseDir: temporary("clay-issues-forged-"),
verifyCommit: function () { calls++; return Promise.resolve({ sha: "c".repeat(40), subject: "fix: valid proof", verifiedAt: Date.now(), repository: "pk_issues" }); },
});
var bound = service.bindProjectSession({ projectSlug: "app", session: f.session });
var issue = await bound.createIssue({ title: "Protected", summary: "Original" });
var forbidden = ["context", "verifiedCommit", "resolutionHistory", "linkedWorkSessions", "visibility"];
for (var i = 0; i < forbidden.length; i++) {
var args = { ref: issue.ref, expectedRevision: 1, status: "resolved", resolutionSummary: "Done", commitSha: "abcdef0" };
args[forbidden[i]] = forbidden[i] === "linkedWorkSessions" || forbidden[i] === "resolutionHistory" ? [] : {};
assert.throws(function (value) { return function () { bound.updateIssue(value); }; }(args), /server-owned|Unknown/);
}
assert.throws(function () {
bound.updateIssue({ ref: "issue:" + "z".repeat(24), expectedRevision: 1, status: "resolved", resolutionSummary: "Done", commitSha: "abcdef0" });
}, /not found/i);
assert.throws(function () {
bound.updateIssue({ ref: issue.ref, expectedRevision: 0, status: "resolved", resolutionSummary: "Done", commitSha: "abcdef0" });
}, /conflict/i);
assert.equal(calls, 0);
var wrongProof = issueService({
getProjects: function () { return f.projects; }, isMultiUser: function () { return true; },
findUserById: function (id) { return id === "u1" ? f.users.user : null; }, canAccessProject: function () { return true; },
baseDir: temporary("clay-issues-wrong-proof-"),
verifyCommit: function () { return { sha: "c".repeat(40), subject: "fix: wrong repository", verifiedAt: Date.now(), repository: "pk_other" }; },
}).bindProjectSession({ projectSlug: "app", session: f.session });
await assert.rejects(wrongProof.createIssue({ title: "Wrong proof", summary: "Denied", resolutionSummary: "Done", status: "resolved", commitSha: "abcdef0" }), /different repository/);
assert.equal(wrongProof.listIssues({}).total, 0);
});
test("bindings retain exact project and session identity and deny malformed or unknown owners", function () {
var f = fixture({ status: { projectOwnerId: "u1" } });
var source = { projectSlug: "app", session: f.session };
var bound = f.service.bindProjectSession(source);
assert.ok(bound);
source.projectSlug = "elsewhere";
source.session = { localId: 99, ownerId: "u1" };
assert.equal(bound.listIssues({}).total, 0);
f.session.ownerId = "owner";
assert.throws(function () { bound.listIssues({}); }, /no longer valid/);
var replacement = fixture({ status: { projectOwnerId: "u1" } });
var replacementBound = replacement.service.bindUser({ projectSlug: "app", userId: "u1" });
replacement.projects.set("app", { getStatus: function () { return replacement.status; } });
assert.throws(function () { replacementBound.listIssues({}); }, /no longer valid/);
var malformedProject = { getStatus: function () { return null; } };
var unknownStatus = { slug: "unknown", path: "/tmp/unknown", projectKnowledgeId: "pk_unknown", projectOwnerId: "ghost", isMate: false };
var unknownProject = { getStatus: function () { return unknownStatus; } };
var projects = new Map([["malformed", malformedProject], ["unknown", unknownProject]]);
var service = issueService({ getProjects: function () { return projects; }, isMultiUser: function () { return true; },
findUserById: function (id) { return id === "u1" ? { id: "u1" } : null; }, canAccessProject: function () { return true; } });
assert.equal(service.bindUser({ projectSlug: "malformed", userId: "u1" }), null);
assert.equal(service.bindUser({ projectSlug: "unknown", userId: "u1" }), null);
var worktreeStatus = { slug: "wt", path: "/tmp/wt", projectKnowledgeId: "pk_wt", projectOwnerId: "u1", isMate: false, isWorktree: true, parentSlug: "broken-parent", changeSetId: "cs-1" };
projects.set("wt", { getStatus: function () { return worktreeStatus; } });
projects.set("broken-parent", { getStatus: function () { throw new Error("broken status"); } });
assert.equal(service.bindUser({ projectSlug: "wt", userId: "u1" }), null);
});
test("single-user ownerless bindings work and OS-user policy is evaluated live", async function () {
var status = { slug: "solo", path: "/tmp/solo", projectKnowledgeId: "pk_solo", projectOwnerId: null, isMate: false, isWorktree: false };
var session = { localId: 1, ownerId: null };
var sessions = new Map([[session.localId, session]]);
var project = { getStatus: function () { return status; }, getSessionManager: function () { return { sessions: sessions }; } };
var enabled = false;
var seenIdentity = "unset";
var service = issueService({
getProjects: function () { return new Map([["solo", project]]); }, isMultiUser: function () { return false; },
osUsersEnabled: function () { return enabled; }, baseDir: temporary("clay-issues-single-"),
verifyCommit: function (cwd, sha, identity, osIdentity) {
seenIdentity = osIdentity;
return Promise.resolve({ sha: "d".repeat(40), subject: "fix: ownerless", verifiedAt: Date.now(), repository: identity });
},
});
var bound = service.bindProjectSession({ projectSlug: "solo", session: session });
assert.ok(bound);
await bound.createIssue({ title: "Ownerless", summary: "Single user", resolutionSummary: "Done", status: "resolved", commitSha: "abcdef0" });
assert.equal(seenIdentity, null);
enabled = true;
assert.throws(function () {
bound.createIssue({ title: "Needs OS", summary: "Denied", resolutionSummary: "Done", status: "resolved", commitSha: "abcdef0" });
}, /OS identity/);
});
test("deferred verification snapshots actor and args and rejects in-place authorization changes", async function () {
var gateResolve;
var gate = new Promise(function (resolve) { gateResolve = resolve; });
var f = fixture({ status: { projectOwnerId: "u1" } });
var service = issueService({
getProjects: function () { return f.projects; }, isMultiUser: function () { return true; },
findUserById: function (id) { return id === "u1" ? f.users.user : null; }, canAccessProject: function () { return true; },
baseDir: temporary("clay-issues-snapshot-"),
verifyCommit: function () { return gate.then(function () { return { sha: "e".repeat(40), subject: "fix: deferred", verifiedAt: Date.now(), repository: "pk_issues" }; }); },
});
var bound = service.bindProjectSession({ projectSlug: "app", session: f.session });
var args = { title: "Snapshot", summary: "Before", resolutionSummary: "Original resolution", status: "resolved", commitSha: "abcdef0" };
var pending = bound.createIssue(args);
f.users.user.displayName = "Mutated Name";
args.title = "Mutated title";
args.resolutionSummary = "Mutated resolution";
gateResolve();
var created = await pending;
assert.equal(created.title, "Snapshot");
assert.equal(created.resolutionSummary, "Original resolution");
assert.equal(created.createdBy.displayName, "User One");
var secondResolve;
var secondGate = new Promise(function (resolve) { secondResolve = resolve; });
var second = fixture({ status: { projectOwnerId: "u1" } });
var secondService = issueService({
getProjects: function () { return second.projects; }, isMultiUser: function () { return true; },
findUserById: function (id) { return id === "u1" ? second.users.user : null; }, canAccessProject: function () { return true; },
baseDir: temporary("clay-issues-inplace-"),
verifyCommit: function () { return secondGate.then(function () { return { sha: "f".repeat(40), subject: "fix: changed auth", verifiedAt: Date.now(), repository: "pk_issues" }; }); },
});
var secondBound = secondService.bindProjectSession({ projectSlug: "app", session: second.session });
var denied = secondBound.createIssue({ title: "Denied", summary: "Pending", resolutionSummary: "Done", status: "resolved", commitSha: "abcdef0" });
second.status.path = "/tmp/retargeted";
secondResolve();
await assert.rejects(denied, /authorization changed/);
assert.equal(secondBound.listIssues({}).total, 0);
});
test("deferred update verification rejects user revocation and exact project replacement", async function () {
async function run(change) {
var release;
var gate = new Promise(function (resolve) { release = resolve; });
var f = fixture({ status: { projectOwnerId: "u1" } });
var liveUser = f.users.user;
var baseDir = temporary("clay-issues-deferred-update-");
var service = issueService({
getProjects: function () { return f.projects; }, isMultiUser: function () { return true; },
findUserById: function (id) { return id === "u1" ? liveUser : null; }, canAccessProject: function () { return true; },
baseDir: baseDir,
verifyCommit: function () { return gate.then(function () { return { sha: "1".repeat(40), subject: "fix: delayed update", verifiedAt: Date.now(), repository: "pk_issues" }; }); },
});
var bound = service.bindProjectSession({ projectSlug: "app", session: f.session });
var issue = await bound.createIssue({ title: "Update target", summary: "Open" });
var args = { ref: issue.ref, expectedRevision: 1, status: "resolved", resolutionSummary: "Finished", commitSha: "abcdef0" };
var pending = bound.updateIssue(args);
args.ref = "issue:" + "q".repeat(24);
args.expectedRevision = 99;
change(f, function () { liveUser = null; });
release();
await assert.rejects(pending, /no longer valid|authorization changed/);
var raw = issueStore.createIssuesStore({ projectKnowledgeId: "pk_issues", baseDir: baseDir });
assert.equal(raw.read(issue.ref).revision, 1);
}
await run(function (f, revoke) { revoke(); });
await run(function (f) {
f.projects.set("app", { getStatus: function () { return f.status; } });
});
});
test("deferred verification rejects an in-place OS identity mutation", async function () {
var release;
var gate = new Promise(function (resolve) { release = resolve; });
var identity = { uid: 501, gid: 20, user: "user-one", home: "/tmp/user-one" };
var f = fixture({ status: { projectOwnerId: "u1" } });
var service = issueService({
getProjects: function () { return f.projects; }, isMultiUser: function () { return true; },
findUserById: function (id) { return id === "u1" ? f.users.user : null; }, canAccessProject: function () { return true; },
osUsersEnabled: true, getOsUserInfoForActor: function () { return identity; },
baseDir: temporary("clay-issues-os-mutation-"),
verifyCommit: function () { return gate.then(function () { return { sha: "2".repeat(40), subject: "fix: OS mutation", verifiedAt: Date.now(), repository: "pk_issues" }; }); },
});
var bound = service.bindProjectSession({ projectSlug: "app", session: f.session });
var pending = bound.createIssue({ title: "OS mutation", summary: "Pending", resolutionSummary: "Done", status: "resolved", commitSha: "abcdef0" });
identity.uid = 502;
release();
await assert.rejects(pending, /authorization changed/);
assert.equal(bound.listIssues({}).total, 0);
});
test("a storage append failure does not advance the visible issue", function () {
var current = store();
var issue = current.create({ title: "Stable", summary: "Before failure" }, author());
var originalAppend = current.records.append;
current.records.append = function () { throw new Error("storage unavailable"); };
assert.throws(function () { current.update(issue.ref, { summary: "After failure" }, 1, author()); }, /storage unavailable/);
current.records.append = originalAppend;
assert.equal(current.read(issue.ref).revision, 1);
assert.equal(current.read(issue.ref).summary, "Before failure");
});