feat: add scoped history and artifacts
Generic Container CI/CD / test-build-publish (push) Failing after 8m25s
Agent CI/CD / build-test-publish-and-deploy (push) Failing after 8m25s

This commit is contained in:
2026-10-22 18:40:20 +08:00
parent bc4e9834e5
commit 7d47a2df86
48 changed files with 2632 additions and 78 deletions
+69
View File
@@ -0,0 +1,69 @@
import { expect, it } from "bun:test";
import express, { type Request } from "express";
import { createHash } from "node:crypto";
import { mkdtemp, mkdir, writeFile, rm, symlink } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { config } from "../../src/config.js";
import { attachTrustedAgentContext, getLocalAgentContext, ProductContextError } from "../../src/context/localContext.js";
import { registerArtifactRoutes } from "../../src/routes/artifact-routes.js";
import { SessionMetadataStore } from "../../src/sessions/metadataStore.js";
it("requires verified actor, project and session ownership for exports and rejects symlinks", async () => {
const originalFetch = globalThis.fetch;
const originalEnv = config.NODE_ENV;
const originalRoot = config.CONVERSATION_WORKSPACE_ROOT;
const root = await mkdtemp(join(tmpdir(), "artifact-route-"));
const store = new SessionMetadataStore(join(root, "metadata"));
const app = express();
let calls = 0;
globalThis.fetch = (async (input: Parameters<typeof fetch>[0], init: Parameters<typeof fetch>[1]) => {
if (String(input).endsWith("/api/v1/agent-auth-context")) {
const headers = new Headers(init?.headers);
return Response.json({ user_id: headers.get("Authorization") === "Bearer OWNER" ? "owner" : "other", project_id: headers.get("X-Project-Id"), network: "supply" });
}
return originalFetch(input, init);
}) as typeof fetch;
config.NODE_ENV = "production";
config.CONVERSATION_WORKSPACE_ROOT = root;
const owner = { header: (name: string) => ({ authorization: "Bearer OWNER", "x-project-id": "project" })[name.toLowerCase() as "authorization" | "x-project-id"] } as Request;
await attachTrustedAgentContext(owner);
const context = getLocalAgentContext(owner);
const session = "export-session";
await store.ensure({ ...context, sessionId: session });
const directory = join(root, context.actorKey, context.projectKey, `session-${createHash("sha256").update(session).digest("hex")}`, "workspace");
await mkdir(join(directory, "files"), { recursive: true });
const filename = "mcp-11111111-1111-1111-1111-111111111111.zip";
await writeFile(join(directory, "files", filename), "fixture");
const router = express.Router();
router.use(async (req, res, next) => {
try { await attachTrustedAgentContext(req); next(); }
catch (error) { res.status(error instanceof ProductContextError ? error.status : 503).end(); }
});
registerArtifactRoutes(router, { sessionMetadataStore: store, runtime: {
ensureSessionWorkspace: async () => { calls++; return directory; },
} });
app.use("/api/v1", router);
const server = app.listen(0, "127.0.0.1");
await new Promise<void>((resolve) => server.once("listening", resolve));
const url = `http://127.0.0.1:${(server.address() as { port: number }).port}/api/v1/session/${session}/files/${filename}`;
const headers = { Authorization: "Bearer OWNER", "X-Project-Id": "project" };
try {
expect((await originalFetch(url)).status).toBe(401);
expect((await originalFetch(url, { headers: { ...headers, Authorization: "Bearer OTHER" } })).status).toBe(404);
expect((await originalFetch(url, { headers: { ...headers, "X-Project-Id": "other" } })).status).toBe(404);
expect(calls).toBe(0);
const response = await originalFetch(url, { headers });
expect(response.status).toBe(200);
expect(response.headers.get("Cache-Control")).toBe("private, no-store");
expect(response.headers.get("Content-Disposition")).toContain("attachment");
expect(await response.text()).toBe("fixture");
await rm(join(directory, "files", filename));
await symlink(join(root, "metadata"), join(directory, "files", filename));
expect((await originalFetch(url, { headers })).status).toBe(404);
} finally {
globalThis.fetch = originalFetch; config.NODE_ENV = originalEnv; config.CONVERSATION_WORKSPACE_ROOT = originalRoot;
await new Promise<void>((resolve) => server.close(() => resolve()));
await rm(root, { recursive: true, force: true });
}
});