Initialize repository with harness code and assets
Initial import of all source code, config, and README assets: the packages workspace (cli, core, server, web, docs, landing, skills), build scripts, tooling config, and CI workflows. Includes the data-layout revision made on this branch: the local data root defaults to ~/.penguin/data (PENGUIN_HOME still overrides; the installer keeps its binaries in ~/.penguin), and every Agent lives under <project>/agents/<agent>/ — path helpers, the three agent-enumeration scans, the system prompt, built-in Skills, tests and docs all follow the new layout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ihk8iQuo3kv2aPjAYEPuR
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Integration tests for createCoreSessionLoader (#3/#13): failures recovering a
|
||||
* historical Session (Workspace deleted / Model removed from config / Trace
|
||||
* missing session_meta) all collapse into HttpError(409, session_unrecoverable),
|
||||
* preserving the original core message instead of bubbling up as a 500.
|
||||
*/
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { createAgent, saveProjectConfig, sessionMeta, userText } from "@prismshadow/penguin-core";
|
||||
import type { SessionMetaPayload } from "@prismshadow/penguin-core";
|
||||
import { createCoreSessionLoader } from "../src/runtime/session-manager.js";
|
||||
import type { SessionRow } from "../src/db/repos/sessions.js";
|
||||
import { HttpError } from "../src/http/errors.js";
|
||||
import { makeTempRoot, writeTraceFile } from "./helpers.js";
|
||||
|
||||
const PROJECT = "project-loader";
|
||||
const AGENT = "default_agent";
|
||||
const SID = "session-2026-07-06-11-00-00-abcd0001";
|
||||
|
||||
function meta(overrides: Partial<SessionMetaPayload> = {}): SessionMetaPayload {
|
||||
return {
|
||||
session_id: SID,
|
||||
model_id: "custom/m1",
|
||||
provider: "custom",
|
||||
model_context_window: 1000,
|
||||
system_prompt: "sp",
|
||||
tools: [],
|
||||
thinking_level: "default",
|
||||
agent_state: "/tmp/a",
|
||||
workspace: path.join("/tmp", "does-not-exist-xyz"),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function row(workspace: string): SessionRow {
|
||||
return {
|
||||
sessionId: SID,
|
||||
projectId: PROJECT,
|
||||
agentId: AGENT,
|
||||
modelId: "custom/m1",
|
||||
provider: "custom",
|
||||
workspace,
|
||||
approvalMode: "always-ask",
|
||||
title: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
describe("session-loader", () => {
|
||||
let root: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
root = await makeTempRoot();
|
||||
// Initialize Agent State (createAgent creates the directory and system_config.yaml).
|
||||
await createAgent({ root, projectId: PROJECT, agentId: AGENT });
|
||||
// Configure Model m1 so recovery doesn't fail on a missing Model (unless the test deletes it on purpose).
|
||||
await saveProjectConfig(root, PROJECT, {
|
||||
default_model: { provider: "custom", model_id: "m1" },
|
||||
models: [{ provider: "custom", model_id: "m1", context_window: 1000 }],
|
||||
});
|
||||
});
|
||||
afterEach(async () => {
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("有 Trace 但 Workspace 已删 → 409 session_unrecoverable,保留原文", async () => {
|
||||
await writeTraceFile(root, PROJECT, AGENT, "2026-07-06", SID, 1, [
|
||||
sessionMeta(meta()), // workspace points to a nonexistent directory
|
||||
userText("hi"),
|
||||
]);
|
||||
const loader = createCoreSessionLoader(root);
|
||||
const err = await loader.load(row("/tmp/does-not-exist-xyz")).catch((e) => e);
|
||||
expect(err).toBeInstanceOf(HttpError);
|
||||
expect((err as HttpError).status).toBe(409);
|
||||
expect((err as HttpError).code).toBe("session_unrecoverable");
|
||||
expect((err as HttpError).message).toContain("Workspace 已不存在");
|
||||
});
|
||||
|
||||
it("有 Trace 但 Model 已从配置移除 → 409 session_unrecoverable", async () => {
|
||||
const ws = path.join(root, "ws");
|
||||
await fs.mkdir(ws, { recursive: true });
|
||||
await writeTraceFile(root, PROJECT, AGENT, "2026-07-06", SID, 1, [
|
||||
sessionMeta(meta({ model_id: "removed-model", workspace: ws })),
|
||||
userText("hi"),
|
||||
]);
|
||||
const loader = createCoreSessionLoader(root);
|
||||
const err = await loader.load(row(ws)).catch((e) => e);
|
||||
expect(err).toBeInstanceOf(HttpError);
|
||||
expect((err as HttpError).status).toBe(409);
|
||||
expect((err as HttpError).code).toBe("session_unrecoverable");
|
||||
expect((err as HttpError).message).toContain("Model 不在 Project 配置中");
|
||||
});
|
||||
|
||||
it("无 Trace 且 Workspace 已删(自愈分支)→ 409 workspace_missing", async () => {
|
||||
const loader = createCoreSessionLoader(root);
|
||||
const err = await loader.load(row("/tmp/gone-workspace-abc")).catch((e) => e);
|
||||
expect(err).toBeInstanceOf(HttpError);
|
||||
expect((err as HttpError).status).toBe(409);
|
||||
expect((err as HttpError).code).toBe("workspace_missing");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user