mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
feat: vendor permission system source
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import {
|
||||
SessionLifecycleHandler,
|
||||
UNTRUSTED_PROJECT_MESSAGE,
|
||||
} from "#src/handlers/lifecycle";
|
||||
import type { ServiceLifecycle } from "#src/service-lifecycle";
|
||||
|
||||
import { makeCtx } from "#test/helpers/handler-fixtures";
|
||||
import {
|
||||
makeLogger,
|
||||
makeRealResolver,
|
||||
makeRealSession,
|
||||
} from "#test/helpers/session-fixtures";
|
||||
|
||||
// ── status stub ────────────────────────────────────────────────────────────
|
||||
vi.mock("../../src/status", () => ({
|
||||
PERMISSION_SYSTEM_STATUS_KEY: "permission-system",
|
||||
syncPermissionSystemStatus: vi.fn(),
|
||||
getPermissionSystemStatus: vi.fn(),
|
||||
}));
|
||||
|
||||
// ── helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
function makeSetup(opts?: { configIssues?: string[] }) {
|
||||
const { session, permissionManager, sessionRules, forwarding, configStore } =
|
||||
makeRealSession();
|
||||
const { resolver } = makeRealResolver(permissionManager, sessionRules);
|
||||
if (opts?.configIssues) {
|
||||
vi.mocked(permissionManager.getConfigIssues).mockReturnValue(
|
||||
opts.configIssues,
|
||||
);
|
||||
}
|
||||
const serviceLifecycle: ServiceLifecycle = {
|
||||
activate: vi.fn<ServiceLifecycle["activate"]>(),
|
||||
teardown: vi.fn<ServiceLifecycle["teardown"]>(),
|
||||
};
|
||||
// Use a session-independent logger so assertions verify direct injection,
|
||||
// not reach-through to session.logger.
|
||||
const logger = makeLogger();
|
||||
const audit = { writeSummary: vi.fn<(logger: unknown) => void>() };
|
||||
const handler = new SessionLifecycleHandler(
|
||||
session,
|
||||
resolver,
|
||||
serviceLifecycle,
|
||||
logger,
|
||||
audit,
|
||||
);
|
||||
return {
|
||||
handler,
|
||||
session,
|
||||
resolver,
|
||||
permissionManager,
|
||||
logger,
|
||||
forwarding,
|
||||
configStore,
|
||||
serviceLifecycle,
|
||||
audit,
|
||||
};
|
||||
}
|
||||
|
||||
// ── handleSessionStart ─────────────────────────────────────────────────────
|
||||
|
||||
describe("handleSessionStart", () => {
|
||||
it("refreshes config with ctx, trusted", async () => {
|
||||
const ctx = makeCtx();
|
||||
const { handler, configStore } = makeSetup();
|
||||
await handler.handleSessionStart({ reason: "startup" }, ctx);
|
||||
expect(configStore.refresh).toHaveBeenCalledWith(ctx, true);
|
||||
});
|
||||
|
||||
it("calls resetForNewSession with ctx, trusted", async () => {
|
||||
const ctx = makeCtx();
|
||||
const { handler, session } = makeSetup();
|
||||
const spy = vi.spyOn(session, "resetForNewSession");
|
||||
await handler.handleSessionStart({ reason: "startup" }, ctx);
|
||||
expect(spy).toHaveBeenCalledWith(ctx, true);
|
||||
});
|
||||
|
||||
describe("project untrusted", () => {
|
||||
function untrustedCtx(): ReturnType<typeof makeCtx> {
|
||||
return makeCtx({
|
||||
isProjectTrusted: vi.fn<() => boolean>().mockReturnValue(false),
|
||||
});
|
||||
}
|
||||
|
||||
it("withholds the project scope from refreshConfig and resetForNewSession", async () => {
|
||||
const ctx = untrustedCtx();
|
||||
const { handler, configStore, session } = makeSetup();
|
||||
const spy = vi.spyOn(session, "resetForNewSession");
|
||||
await handler.handleSessionStart({ reason: "startup" }, ctx);
|
||||
expect(configStore.refresh).toHaveBeenCalledWith(ctx, false);
|
||||
expect(spy).toHaveBeenCalledWith(ctx, false);
|
||||
});
|
||||
|
||||
it("loudly warns and records a review entry when untrusted", async () => {
|
||||
const ctx = untrustedCtx();
|
||||
const { handler, logger } = makeSetup();
|
||||
await handler.handleSessionStart({ reason: "startup" }, ctx);
|
||||
expect(logger.warn).toHaveBeenCalledWith(UNTRUSTED_PROJECT_MESSAGE);
|
||||
expect(logger.review).toHaveBeenCalledWith("project_trust.skipped", {
|
||||
cwd: ctx.cwd,
|
||||
phase: "session_start",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not warn when the project is trusted", async () => {
|
||||
const { handler, logger } = makeSetup();
|
||||
await handler.handleSessionStart({ reason: "startup" }, makeCtx());
|
||||
expect(logger.warn).not.toHaveBeenCalledWith(UNTRUSTED_PROJECT_MESSAGE);
|
||||
});
|
||||
});
|
||||
|
||||
it("logs resolved config paths", async () => {
|
||||
const { handler, configStore } = makeSetup();
|
||||
await handler.handleSessionStart({ reason: "startup" }, makeCtx());
|
||||
expect(configStore.logResolvedPaths).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("resolves agent name from ctx", async () => {
|
||||
const ctx = makeCtx();
|
||||
const { handler, session } = makeSetup();
|
||||
const spy = vi.spyOn(session, "resolveAgentName");
|
||||
await handler.handleSessionStart({ reason: "startup" }, ctx);
|
||||
expect(spy).toHaveBeenCalledWith(ctx);
|
||||
});
|
||||
|
||||
it("notifies each policy issue", async () => {
|
||||
const { handler, logger } = makeSetup({
|
||||
configIssues: ["issue A", "issue B"],
|
||||
});
|
||||
await handler.handleSessionStart({ reason: "startup" }, makeCtx());
|
||||
expect(logger.warn).toHaveBeenCalledWith("issue A");
|
||||
expect(logger.warn).toHaveBeenCalledWith("issue B");
|
||||
});
|
||||
|
||||
it("does not warn when there are no policy issues", async () => {
|
||||
const { handler, logger } = makeSetup();
|
||||
await handler.handleSessionStart({ reason: "startup" }, makeCtx());
|
||||
expect(logger.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("writes lifecycle.reload debug log when reason is reload", async () => {
|
||||
const ctx = makeCtx({ cwd: "/proj" });
|
||||
const { handler, logger } = makeSetup();
|
||||
await handler.handleSessionStart({ reason: "reload" }, ctx);
|
||||
expect(logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
|
||||
triggeredBy: "session_start",
|
||||
reason: "reload",
|
||||
cwd: "/proj",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not write lifecycle.reload debug log for non-reload reasons", async () => {
|
||||
const { handler, logger } = makeSetup();
|
||||
await handler.handleSessionStart({ reason: "startup" }, makeCtx());
|
||||
expect(logger.debug).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("activates the service for the session with ctx", async () => {
|
||||
const ctx = makeCtx();
|
||||
const { handler, serviceLifecycle } = makeSetup();
|
||||
await handler.handleSessionStart({ reason: "startup" }, ctx);
|
||||
expect(serviceLifecycle.activate).toHaveBeenCalledWith(ctx);
|
||||
});
|
||||
|
||||
it("calls refreshConfig before resetForNewSession", async () => {
|
||||
const callOrder: string[] = [];
|
||||
const { handler, session, configStore } = makeSetup();
|
||||
vi.spyOn(configStore, "refresh").mockImplementation(() => {
|
||||
callOrder.push("refreshConfig");
|
||||
});
|
||||
vi.spyOn(session, "resetForNewSession").mockImplementation(() => {
|
||||
callOrder.push("resetForNewSession");
|
||||
});
|
||||
await handler.handleSessionStart({ reason: "startup" }, makeCtx());
|
||||
expect(callOrder).toEqual(["refreshConfig", "resetForNewSession"]);
|
||||
});
|
||||
});
|
||||
|
||||
// ── handleResourcesDiscover ────────────────────────────────────────────────
|
||||
|
||||
describe("handleResourcesDiscover", () => {
|
||||
it("does nothing when reason is not reload", async () => {
|
||||
const { handler, session } = makeSetup();
|
||||
const spy = vi.spyOn(session, "reload");
|
||||
await handler.handleResourcesDiscover({ reason: "startup" }, makeCtx());
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reloads the session with the trust flag on reload", async () => {
|
||||
const { handler, session } = makeSetup();
|
||||
const spy = vi.spyOn(session, "reload");
|
||||
await handler.handleResourcesDiscover({ reason: "reload" }, makeCtx());
|
||||
expect(spy).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it("withholds the project scope and warns on an untrusted reload", async () => {
|
||||
const ctx = makeCtx({
|
||||
cwd: "/proj",
|
||||
isProjectTrusted: vi.fn<() => boolean>().mockReturnValue(false),
|
||||
});
|
||||
const { handler, session, logger } = makeSetup();
|
||||
const spy = vi.spyOn(session, "reload");
|
||||
await handler.handleResourcesDiscover({ reason: "reload" }, ctx);
|
||||
expect(spy).toHaveBeenCalledWith(false);
|
||||
expect(logger.warn).toHaveBeenCalledWith(UNTRUSTED_PROJECT_MESSAGE);
|
||||
expect(logger.review).toHaveBeenCalledWith("project_trust.skipped", {
|
||||
cwd: "/proj",
|
||||
phase: "resources_discover",
|
||||
});
|
||||
});
|
||||
|
||||
it("writes lifecycle.reload debug log on reload", async () => {
|
||||
const ctx = makeCtx({ cwd: "/proj" });
|
||||
const { handler, session, logger } = makeSetup();
|
||||
session.activate(ctx);
|
||||
await handler.handleResourcesDiscover({ reason: "reload" }, ctx);
|
||||
expect(logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
|
||||
triggeredBy: "resources_discover",
|
||||
reason: "reload",
|
||||
cwd: "/proj",
|
||||
});
|
||||
});
|
||||
|
||||
it("logs cwd as null when runtimeContext is null on reload", async () => {
|
||||
const { handler, logger } = makeSetup();
|
||||
await handler.handleResourcesDiscover({ reason: "reload" }, makeCtx());
|
||||
expect(logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
|
||||
triggeredBy: "resources_discover",
|
||||
reason: "reload",
|
||||
cwd: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ── handleSessionShutdown ──────────────────────────────────────────────────
|
||||
|
||||
describe("handleSessionShutdown", () => {
|
||||
it("clears UI status when runtime context is present", async () => {
|
||||
const ctx = makeCtx();
|
||||
const { handler, session } = makeSetup();
|
||||
session.activate(ctx);
|
||||
await handler.handleSessionShutdown();
|
||||
expect(ctx.ui.setStatus).toHaveBeenCalledWith(
|
||||
"permission-system",
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("does not throw when runtime context is null", async () => {
|
||||
const { handler } = makeSetup();
|
||||
await expect(handler.handleSessionShutdown()).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it("calls shutdown on the session", async () => {
|
||||
const { handler, session } = makeSetup();
|
||||
const spy = vi.spyOn(session, "shutdown");
|
||||
await handler.handleSessionShutdown();
|
||||
expect(spy).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("calls serviceLifecycle.teardown", async () => {
|
||||
const { handler, serviceLifecycle } = makeSetup();
|
||||
await handler.handleSessionShutdown();
|
||||
expect(serviceLifecycle.teardown).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("writes the decision-audit summary to the logger", async () => {
|
||||
const { handler, audit, logger } = makeSetup();
|
||||
await handler.handleSessionShutdown();
|
||||
expect(audit.writeSummary).toHaveBeenCalledWith(logger);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user