mirror of
https://github.com/mksglu/context-mode.git
synced 2026-10-03 04:38:25 +08:00
refactor(adapter/C2): drop getSessionDBPath/EventsPath from Adapter interface
The two methods were SHALLOW pure derivatives of `getSessionDir() +
projectDir` (interface complexity == implementation complexity). The Codex
override added no behavior — its `normalizeWorktreePath` pre-pass was a
no-op because `resolveSessionDbPath` and `hashProjectDirCanonical` already
normalize internally. The events.md override included a worktree suffix
that nothing in production consumed (server.ts and hooks build their own
events.md path inline).
Apply the deletion test: removing them from BaseAdapter / Adapter / Codex
collapses to ONE site of computation (`resolveSessionDbPath` in
src/session/db.ts). Complexity does NOT reappear in N callers — only one
production caller existed (opencode/plugin.ts), updated to call the helper
directly. Adapter authors can no longer cargo-cult overrides; the methods
do not exist on the interface.
Migration shape (preserved by tests):
before: new SessionDB({ dbPath: adapter.getSessionDBPath(projectDir) })
after: new SessionDB({ dbPath: resolveSessionDbPath({
projectDir, sessionsDir: adapter.getSessionDir(),
}) })
Test counts: 2627→2630 passed (+2 new C2 narrowing pins, +1 less flake);
19 baseline failures unchanged (security policy + statusline cross-OS,
unrelated to this change).
Cross-OS validation: `tests/hooks/precompact-snapshot-event.test.ts:81-84`
documents the slash-folding pattern that resolveSessionDbPath performs
internally; refs/platforms/codex/codex-rs/core/src/config/mod.rs:622-627
confirms upstream Codex separates platform dir state (`codex_home`) from
per-project file paths — the separation we are formalizing.
Bundles regenerated via `npm run build`. hooks/*.bundle.mjs unchanged
(they don't import adapter classes).
This commit is contained in:
+252
-252
File diff suppressed because one or more lines are too long
+214
-214
File diff suppressed because one or more lines are too long
+8
-22
@@ -1,13 +1,10 @@
|
||||
/**
|
||||
* BaseAdapter — shared implementation for methods identical across all adapters.
|
||||
*
|
||||
* Eliminates ~288 lines of duplication across 12 adapters.
|
||||
* Each concrete adapter extends this and provides platform-specific logic.
|
||||
*
|
||||
* Shared methods:
|
||||
* - getSessionDir() — builds session dir from sessionDirSegments
|
||||
* - getSessionDBPath() — SHA-256 hash of projectDir → .db file
|
||||
* - getSessionEventsPath()— SHA-256 hash of projectDir → -events.md file
|
||||
* - backupSettings() — copies settings file to .bak
|
||||
*
|
||||
* Adapters with custom logic override the relevant method:
|
||||
@@ -15,12 +12,19 @@
|
||||
* - opencode: overrides getSessionDir (XDG_CONFIG_HOME / APPDATA)
|
||||
* and backupSettings (calls checkPluginRegistration first)
|
||||
* - openclaw: overrides backupSettings (searches 3 config paths)
|
||||
*
|
||||
* NOTE — C2 narrowing (2026-05): `getSessionDBPath` and `getSessionEventsPath`
|
||||
* were removed. Both were SHALLOW pure derivatives of `getSessionDir() +
|
||||
* projectDir` (interface complexity == implementation complexity). All
|
||||
* adapter-storage path computation now flows through ONE site:
|
||||
* `resolveSessionDbPath({ projectDir, sessionsDir: adapter.getSessionDir() })`
|
||||
* in `src/session/db.ts`. Adapters expose only `getSessionDir()` for
|
||||
* storage-related path concerns.
|
||||
*/
|
||||
|
||||
import { join } from "node:path";
|
||||
import { accessSync, copyFileSync, constants, mkdirSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { hashProjectDirCanonical, resolveSessionDbPath } from "../session/db.js";
|
||||
|
||||
export abstract class BaseAdapter {
|
||||
constructor(protected readonly sessionDirSegments: string[]) {}
|
||||
@@ -31,24 +35,6 @@ export abstract class BaseAdapter {
|
||||
return dir;
|
||||
}
|
||||
|
||||
getSessionDBPath(projectDir: string): string {
|
||||
// Delegates to resolveSessionDbPath for case-fold + worktree-suffix
|
||||
// handling and one-shot migration of legacy raw-casing files. All 12
|
||||
// adapters share this so cross-adapter ctx_stats reads see the same
|
||||
// file the writer produced.
|
||||
return resolveSessionDbPath({
|
||||
projectDir,
|
||||
sessionsDir: this.getSessionDir(),
|
||||
});
|
||||
}
|
||||
|
||||
getSessionEventsPath(projectDir: string): string {
|
||||
// Sidecar to getSessionDBPath — same canonical hash so they live next
|
||||
// to each other on disk. No migration helper for .md sidecars yet
|
||||
// (they get rewritten on every session); the canonical hash is enough.
|
||||
return join(this.getSessionDir(), `${hashProjectDirCanonical(projectDir)}-events.md`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default: build config dir from sessionDirSegments rooted at $HOME.
|
||||
*
|
||||
|
||||
@@ -26,7 +26,6 @@ import { resolve, dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { BaseAdapter } from "../base.js";
|
||||
import { getWorktreeSuffix, hashProjectDirCanonical, normalizeWorktreePath, resolveSessionDbPath } from "../../session/db.js";
|
||||
import { resolveCodexConfigDir } from "./paths.js";
|
||||
|
||||
import {
|
||||
@@ -312,20 +311,12 @@ export class CodexAdapter extends BaseAdapter implements HookAdapter {
|
||||
return dir;
|
||||
}
|
||||
|
||||
getSessionDBPath(projectDir: string): string {
|
||||
return resolveSessionDbPath({
|
||||
projectDir: normalizeWorktreePath(projectDir),
|
||||
sessionsDir: this.getSessionDir(),
|
||||
});
|
||||
}
|
||||
|
||||
getSessionEventsPath(projectDir: string): string {
|
||||
const normalized = normalizeWorktreePath(projectDir);
|
||||
return join(
|
||||
this.getSessionDir(),
|
||||
`${hashProjectDirCanonical(normalized)}${getWorktreeSuffix(normalized)}-events.md`,
|
||||
);
|
||||
}
|
||||
// C2 narrowing (2026-05): the historical `getSessionDBPath` /
|
||||
// `getSessionEventsPath` overrides were removed. Both delegated to the
|
||||
// same canonical helpers (`resolveSessionDbPath` / `hashProjectDirCanonical`
|
||||
// + `getWorktreeSuffix`) which already normalize the path internally —
|
||||
// the explicit `normalizeWorktreePath` here was a no-op. Callers now reach
|
||||
// the helpers directly through `adapter.getSessionDir()`.
|
||||
|
||||
getInstructionFiles(): string[] {
|
||||
// Codex CLI honors AGENTS.md plus an optional override file.
|
||||
|
||||
@@ -25,7 +25,7 @@ import { dirname, resolve, join } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
|
||||
import { SessionDB } from "../../session/db.js";
|
||||
import { resolveSessionDbPath, SessionDB } from "../../session/db.js";
|
||||
import { extractEvents, extractUserEvents } from "../../session/extract.js";
|
||||
import type { HookInput } from "../../session/extract.js";
|
||||
import { buildResumeSnapshot } from "../../session/snapshot.js";
|
||||
@@ -281,7 +281,13 @@ async function createContextModePlugin(ctx: PluginContext) {
|
||||
// process-global UUID would (a) never match prior-session resume rows and
|
||||
// (b) collide across multi-session reuse (Mickey / PR #376 root cause).
|
||||
const projectDir = ctx?.directory ?? process.cwd();
|
||||
const db = new SessionDB({ dbPath: adapter.getSessionDBPath(projectDir) });
|
||||
// C2 narrowing: resolve DB path through the canonical helper directly.
|
||||
// BaseAdapter no longer exposes getSessionDBPath; the adapter only owns
|
||||
// the sessions DIR (per-platform), the helper owns the per-project FILE
|
||||
// (case-fold + worktree-suffix + one-shot legacy migration).
|
||||
const db = new SessionDB({
|
||||
dbPath: resolveSessionDbPath({ projectDir, sessionsDir: adapter.getSessionDir() }),
|
||||
});
|
||||
|
||||
// Clean up old sessions on startup (no SessionStart hook to do this).
|
||||
db.cleanupOldSessions(7);
|
||||
|
||||
+10
-7
@@ -209,15 +209,18 @@ export interface HookAdapter {
|
||||
/** Path to the platform's settings file (e.g., ~/.claude/settings.json). */
|
||||
getSettingsPath(): string;
|
||||
|
||||
/** Directory where session data is stored. */
|
||||
/**
|
||||
* Directory where session data is stored.
|
||||
*
|
||||
* NOTE — C2 narrowing (2026-05): this is the ONLY storage-path concern an
|
||||
* adapter exposes. Per-project DB paths are derived by callers via
|
||||
* `resolveSessionDbPath({ projectDir, sessionsDir: adapter.getSessionDir() })`
|
||||
* (see `src/session/db.ts`). Per-project events.md paths follow the same
|
||||
* `<sessionDir>/<hash><suffix>-events.md` shape and are computed inline at
|
||||
* the small number of call sites that need them (server.ts, hooks).
|
||||
*/
|
||||
getSessionDir(): string;
|
||||
|
||||
/** Compute per-project session DB path. */
|
||||
getSessionDBPath(projectDir: string): string;
|
||||
|
||||
/** Compute per-project session events file path. */
|
||||
getSessionEventsPath(projectDir: string): string;
|
||||
|
||||
/**
|
||||
* Platform config directory.
|
||||
*
|
||||
|
||||
@@ -3,6 +3,7 @@ import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { homedir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { AntigravityAdapter } from "../../src/adapters/antigravity/index.js";
|
||||
import { hashProjectDirCanonical, resolveSessionDbPath } from "../../src/session/db.js";
|
||||
|
||||
describe("AntigravityAdapter", () => {
|
||||
let adapter: AntigravityAdapter;
|
||||
@@ -134,13 +135,13 @@ describe("AntigravityAdapter", () => {
|
||||
});
|
||||
|
||||
it("session DB path contains project hash", () => {
|
||||
const dbPath = adapter.getSessionDBPath("/test/project");
|
||||
const dbPath = resolveSessionDbPath({ projectDir: "/test/project", sessionsDir: adapter.getSessionDir() });
|
||||
expect(dbPath).toMatch(/[a-f0-9]{16}\.db$/);
|
||||
expect(dbPath).toContain(".gemini");
|
||||
});
|
||||
|
||||
it("session events path contains project hash with -events.md suffix", () => {
|
||||
const eventsPath = adapter.getSessionEventsPath("/test/project");
|
||||
const eventsPath = join(adapter.getSessionDir(), `${hashProjectDirCanonical("/test/project")}-events.md`);
|
||||
expect(eventsPath).toMatch(/[a-f0-9]{16}-events\.md$/);
|
||||
expect(eventsPath).toContain(".gemini");
|
||||
});
|
||||
|
||||
@@ -45,3 +45,26 @@ describe("BaseAdapter memory/config defaults", () => {
|
||||
expect(adapter.getMemoryDir()).toBe(join(homedir(), ".claude", "memory"));
|
||||
});
|
||||
});
|
||||
|
||||
// C2 narrowing — BaseAdapter MUST NOT expose path helpers that are pure
|
||||
// derivatives of `getSessionDir() + projectDir`. Those derivatives belong
|
||||
// in `src/session/db.ts:resolveSessionDbPath` (single site of computation,
|
||||
// case-fold migration, worktree-suffix handling). Exposing them on every
|
||||
// adapter is a SHALLOW interface — its complexity equals its implementation
|
||||
// — and tempts adapter authors to override for cargo-cult reasons (e.g. the
|
||||
// pre-narrowing CodexAdapter override that just delegated to the same
|
||||
// helper). Deletion test: collapses to ONE call site, complexity does NOT
|
||||
// reappear in N callers.
|
||||
describe("BaseAdapter — adapter-storage interface narrowing (C2)", () => {
|
||||
it("does NOT expose getSessionDBPath — callers go through resolveSessionDbPath", () => {
|
||||
const adapter = new TestAdapter([".claude"]);
|
||||
// Use Reflect to interrogate the runtime shape — the cast is intentional;
|
||||
// we are pinning that the public surface no longer carries this method.
|
||||
expect((adapter as unknown as Record<string, unknown>).getSessionDBPath).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does NOT expose getSessionEventsPath — events.md path lives in callers/server", () => {
|
||||
const adapter = new TestAdapter([".claude"]);
|
||||
expect((adapter as unknown as Record<string, unknown>).getSessionEventsPath).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import "../setup-home";
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { createHash } from "node:crypto";
|
||||
import { homedir, tmpdir } from "node:os";
|
||||
import { join, resolve, sep } from "node:path";
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { ClaudeCodeAdapter } from "../../src/adapters/claude-code/index.js";
|
||||
import { hashProjectDirCanonical, resolveSessionDbPath } from "../../src/session/db.js";
|
||||
import { fakeHome, realHome } from "../setup-home";
|
||||
import {
|
||||
PRE_TOOL_USE_MATCHERS,
|
||||
@@ -231,13 +231,15 @@ describe("ClaudeCodeAdapter", () => {
|
||||
expect(sessionDir.startsWith(join(realHome, ".claude", "context-mode"))).toBe(false);
|
||||
});
|
||||
|
||||
it("DB path uses sha256 hash of projectDir", () => {
|
||||
// C2 narrowing: per-project DB path is composed by callers via
|
||||
// resolveSessionDbPath + adapter.getSessionDir().
|
||||
it("DB path uses canonical hash of projectDir", () => {
|
||||
const projectDir = "/my/project";
|
||||
const hash = createHash("sha256")
|
||||
.update(projectDir)
|
||||
.digest("hex")
|
||||
.slice(0, 16);
|
||||
const dbPath = adapter.getSessionDBPath(projectDir);
|
||||
const hash = hashProjectDirCanonical(projectDir);
|
||||
const dbPath = resolveSessionDbPath({
|
||||
projectDir,
|
||||
sessionsDir: adapter.getSessionDir(),
|
||||
});
|
||||
expect(dbPath).toBe(
|
||||
join(homedir(), ".claude", "context-mode", "sessions", `${hash}.db`),
|
||||
);
|
||||
@@ -267,14 +269,19 @@ describe("ClaudeCodeAdapter", () => {
|
||||
rmSync(customDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("getSessionDBPath lands the DB under $CLAUDE_CONFIG_DIR (not ~/.claude)", () => {
|
||||
// C2 narrowing (2026-05): the test now composes the DB path through
|
||||
// resolveSessionDbPath + adapter.getSessionDir() — this is the SAME
|
||||
// composition production callers (server.ts, opencode plugin, hooks)
|
||||
// perform. The regression pin still holds: $CLAUDE_CONFIG_DIR must
|
||||
// route the file out of ~/.claude.
|
||||
it("DB path lands under $CLAUDE_CONFIG_DIR (not ~/.claude)", () => {
|
||||
process.env.CLAUDE_CONFIG_DIR = customDir;
|
||||
const projectDir = "/test/project";
|
||||
const hash = createHash("sha256")
|
||||
.update(projectDir)
|
||||
.digest("hex")
|
||||
.slice(0, 16);
|
||||
const dbPath = adapter.getSessionDBPath(projectDir);
|
||||
const hash = hashProjectDirCanonical(projectDir);
|
||||
const dbPath = resolveSessionDbPath({
|
||||
projectDir,
|
||||
sessionsDir: adapter.getSessionDir(),
|
||||
});
|
||||
expect(dbPath).toBe(
|
||||
join(customDir, "context-mode", "sessions", `${hash}.db`),
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync,
|
||||
import { homedir, tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { CodexAdapter } from "../../src/adapters/codex/index.js";
|
||||
import { SessionDB } from "../../src/session/db.js";
|
||||
import { resolveSessionDbPath, SessionDB } from "../../src/session/db.js";
|
||||
|
||||
describe("CodexAdapter", () => {
|
||||
let adapter: CodexAdapter;
|
||||
@@ -605,7 +605,10 @@ describe("Codex precompact hook script", () => {
|
||||
process.env.CODEX_HOME = codexHome;
|
||||
|
||||
try {
|
||||
const dbPath = new CodexAdapter().getSessionDBPath(projectDir);
|
||||
const dbPath = resolveSessionDbPath({
|
||||
projectDir,
|
||||
sessionsDir: new CodexAdapter().getSessionDir(),
|
||||
});
|
||||
const db = new SessionDB({ dbPath });
|
||||
db.ensureSession(sessionId, projectDir);
|
||||
db.insertEvent(sessionId, {
|
||||
@@ -664,7 +667,10 @@ describe("Codex sessionstart hook script", () => {
|
||||
process.env.CODEX_HOME = codexHome;
|
||||
|
||||
try {
|
||||
const dbPath = new CodexAdapter().getSessionDBPath(projectDir);
|
||||
const dbPath = resolveSessionDbPath({
|
||||
projectDir,
|
||||
sessionsDir: new CodexAdapter().getSessionDir(),
|
||||
});
|
||||
const db = new SessionDB({ dbPath });
|
||||
db.ensureSession(sessionId, projectDir);
|
||||
db.upsertResume(sessionId, snapshot, 1);
|
||||
|
||||
@@ -2,8 +2,8 @@ import "../setup-home";
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { homedir } from "node:os";
|
||||
import { resolve, join } from "node:path";
|
||||
import { createHash } from "node:crypto";
|
||||
import { JetBrainsCopilotAdapter } from "../../src/adapters/jetbrains-copilot/index.js";
|
||||
import { hashProjectDirCanonical, resolveSessionDbPath } from "../../src/session/db.js";
|
||||
|
||||
describe("JetBrainsCopilotAdapter", () => {
|
||||
let adapter: JetBrainsCopilotAdapter;
|
||||
@@ -60,17 +60,18 @@ describe("JetBrainsCopilotAdapter", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── getSessionDBPath ──────────────────────────────────
|
||||
// ── per-project DB path (C2 narrowing) ────────────────
|
||||
// BaseAdapter no longer exposes getSessionDBPath; callers go through
|
||||
// resolveSessionDbPath + adapter.getSessionDir(). These pins assert that
|
||||
// composition lands the .db inside JetBrains' sessionDir with the
|
||||
// canonical project hash.
|
||||
|
||||
describe("getSessionDBPath", () => {
|
||||
describe("per-project DB path via resolveSessionDbPath", () => {
|
||||
it("produces correct hash-based path", () => {
|
||||
const projectDir = "/home/user/my-project";
|
||||
const dbPath = adapter.getSessionDBPath(projectDir);
|
||||
const dbPath = resolveSessionDbPath({ projectDir, sessionsDir: adapter.getSessionDir() });
|
||||
|
||||
const expectedHash = createHash("sha256")
|
||||
.update(projectDir)
|
||||
.digest("hex")
|
||||
.slice(0, 16);
|
||||
const expectedHash = hashProjectDirCanonical(projectDir);
|
||||
|
||||
expect(dbPath).toContain(expectedHash);
|
||||
expect(dbPath).toMatch(/\.db$/);
|
||||
@@ -79,8 +80,8 @@ describe("JetBrainsCopilotAdapter", () => {
|
||||
});
|
||||
|
||||
it("produces different paths for different project dirs", () => {
|
||||
const path1 = adapter.getSessionDBPath("/project/a");
|
||||
const path2 = adapter.getSessionDBPath("/project/b");
|
||||
const path1 = resolveSessionDbPath({ projectDir: "/project/a", sessionsDir: adapter.getSessionDir() });
|
||||
const path2 = resolveSessionDbPath({ projectDir: "/project/b", sessionsDir: adapter.getSessionDir() });
|
||||
expect(path1).not.toBe(path2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { homedir, tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { mkdtempSync, rmSync, existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
||||
import { KiroAdapter } from "../../src/adapters/kiro/index.js";
|
||||
import { hashProjectDirCanonical, resolveSessionDbPath } from "../../src/session/db.js";
|
||||
import {
|
||||
PRE_TOOL_USE_MATCHER_PATTERN,
|
||||
PRE_TOOL_USE_MATCHERS,
|
||||
@@ -253,14 +254,24 @@ describe("KiroAdapter", () => {
|
||||
);
|
||||
});
|
||||
|
||||
// C2 narrowing: per-project DB path is computed by callers via
|
||||
// resolveSessionDbPath + adapter.getSessionDir(). Test pins that the
|
||||
// composition lands the file inside Kiro's sessionDir (~/.kiro/...).
|
||||
it("session DB path contains project hash", () => {
|
||||
const dbPath = adapter.getSessionDBPath("/test/project");
|
||||
const dbPath = resolveSessionDbPath({
|
||||
projectDir: "/test/project",
|
||||
sessionsDir: adapter.getSessionDir(),
|
||||
});
|
||||
expect(dbPath).toMatch(/[a-f0-9]{16}\.db$/);
|
||||
expect(dbPath).toContain(".kiro");
|
||||
});
|
||||
|
||||
it("session events path contains project hash with -events.md suffix", () => {
|
||||
const eventsPath = adapter.getSessionEventsPath("/test/project");
|
||||
// events.md sidecar shape mirrors server.ts/hooks: <sessionDir>/<hash>-events.md
|
||||
const eventsPath = join(
|
||||
adapter.getSessionDir(),
|
||||
`${hashProjectDirCanonical("/test/project")}-events.md`,
|
||||
);
|
||||
expect(eventsPath).toMatch(/[a-f0-9]{16}-events\.md$/);
|
||||
expect(eventsPath).toContain(".kiro");
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { homedir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { OMPAdapter } from "../../src/adapters/omp/index.js";
|
||||
import { hashProjectDirCanonical, resolveSessionDbPath } from "../../src/session/db.js";
|
||||
|
||||
describe("OMPAdapter", () => {
|
||||
let adapter: OMPAdapter;
|
||||
@@ -132,14 +133,14 @@ describe("OMPAdapter", () => {
|
||||
});
|
||||
|
||||
it("session DB path contains project hash and lives under .omp", () => {
|
||||
const dbPath = adapter.getSessionDBPath("/test/project");
|
||||
const dbPath = resolveSessionDbPath({ projectDir: "/test/project", sessionsDir: adapter.getSessionDir() });
|
||||
expect(dbPath).toMatch(/[a-f0-9]{16}\.db$/);
|
||||
expect(dbPath).toContain(".omp");
|
||||
expect(dbPath).not.toContain(".claude");
|
||||
});
|
||||
|
||||
it("session events path contains project hash and lives under .omp", () => {
|
||||
const eventsPath = adapter.getSessionEventsPath("/test/project");
|
||||
const eventsPath = join(adapter.getSessionDir(), `${hashProjectDirCanonical("/test/project")}-events.md`);
|
||||
expect(eventsPath).toMatch(/[a-f0-9]{16}-events\.md$/);
|
||||
expect(eventsPath).toContain(".omp");
|
||||
expect(eventsPath).not.toContain(".claude");
|
||||
|
||||
@@ -3,6 +3,7 @@ import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { homedir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { OpenClawAdapter } from "../../src/adapters/openclaw/index.js";
|
||||
import { hashProjectDirCanonical, resolveSessionDbPath } from "../../src/session/db.js";
|
||||
|
||||
describe("OpenClawAdapter", () => {
|
||||
let adapter: OpenClawAdapter;
|
||||
@@ -276,7 +277,7 @@ describe("OpenClawAdapter", () => {
|
||||
});
|
||||
|
||||
it("session DB path includes project hash", () => {
|
||||
const dbPath = adapter.getSessionDBPath("/test/project");
|
||||
const dbPath = resolveSessionDbPath({ projectDir: "/test/project", sessionsDir: adapter.getSessionDir() });
|
||||
expect(dbPath).toContain(".openclaw");
|
||||
expect(dbPath).toContain("context-mode");
|
||||
expect(dbPath).toContain("sessions");
|
||||
@@ -284,7 +285,7 @@ describe("OpenClawAdapter", () => {
|
||||
});
|
||||
|
||||
it("session events path includes project hash", () => {
|
||||
const eventsPath = adapter.getSessionEventsPath("/test/project");
|
||||
const eventsPath = join(adapter.getSessionDir(), `${hashProjectDirCanonical("/test/project")}-events.md`);
|
||||
expect(eventsPath).toContain(".openclaw");
|
||||
expect(eventsPath).toMatch(/-events\.md$/);
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import { join, resolve } from "node:path";
|
||||
import { PiAdapter } from "../../src/adapters/pi/index.js";
|
||||
import { ClaudeCodeAdapter } from "../../src/adapters/claude-code/index.js";
|
||||
import { getAdapter, getSessionDirSegments } from "../../src/adapters/detect.js";
|
||||
import { hashProjectDirCanonical, resolveSessionDbPath } from "../../src/session/db.js";
|
||||
|
||||
describe("PiAdapter — Pi platform adapter", () => {
|
||||
let adapter: PiAdapter;
|
||||
@@ -73,14 +74,14 @@ describe("PiAdapter — Pi platform adapter", () => {
|
||||
});
|
||||
|
||||
it("session DB path contains project hash and lives under .pi", () => {
|
||||
const dbPath = adapter.getSessionDBPath("/test/project");
|
||||
const dbPath = resolveSessionDbPath({ projectDir: "/test/project", sessionsDir: adapter.getSessionDir() });
|
||||
expect(dbPath).toMatch(/[a-f0-9]{16}\.db$/);
|
||||
expect(dbPath).toContain(".pi");
|
||||
expect(dbPath).not.toContain(".claude");
|
||||
});
|
||||
|
||||
it("session events path contains project hash and lives under .pi", () => {
|
||||
const eventsPath = adapter.getSessionEventsPath("/test/project");
|
||||
const eventsPath = join(adapter.getSessionDir(), `${hashProjectDirCanonical("/test/project")}-events.md`);
|
||||
expect(eventsPath).toMatch(/[a-f0-9]{16}-events\.md$/);
|
||||
expect(eventsPath).toContain(".pi");
|
||||
expect(eventsPath).not.toContain(".claude");
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import "../setup-home";
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { createHash } from "node:crypto";
|
||||
import { homedir, tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { QwenCodeAdapter } from "../../src/adapters/qwen-code/index.js";
|
||||
import { hashProjectDirCanonical, resolveSessionDbPath } from "../../src/session/db.js";
|
||||
import { fakeHome, realHome } from "../setup-home";
|
||||
|
||||
describe("QwenCodeAdapter", () => {
|
||||
@@ -156,13 +156,15 @@ describe("QwenCodeAdapter", () => {
|
||||
expect(sessionDir.startsWith(join(realHome, ".qwen", "context-mode"))).toBe(false);
|
||||
});
|
||||
|
||||
it("DB path uses sha256 hash of projectDir", () => {
|
||||
// C2 narrowing: per-project DB path is composed by callers via
|
||||
// resolveSessionDbPath + adapter.getSessionDir().
|
||||
it("DB path uses canonical hash of projectDir", () => {
|
||||
const projectDir = "/my/project";
|
||||
const hash = createHash("sha256")
|
||||
.update(projectDir)
|
||||
.digest("hex")
|
||||
.slice(0, 16);
|
||||
const dbPath = adapter.getSessionDBPath(projectDir);
|
||||
const hash = hashProjectDirCanonical(projectDir);
|
||||
const dbPath = resolveSessionDbPath({
|
||||
projectDir,
|
||||
sessionsDir: adapter.getSessionDir(),
|
||||
});
|
||||
expect(dbPath).toBe(
|
||||
join(homedir(), ".qwen", "context-mode", "sessions", `${hash}.db`),
|
||||
);
|
||||
|
||||
@@ -2292,10 +2292,15 @@ describe("Platform-aware session paths via adapter", () => {
|
||||
expect(statsMatch![0]).not.toMatch(/["']\.claude["']/);
|
||||
});
|
||||
|
||||
// ── Adapter methods used for session paths ──
|
||||
test("session paths derived from adapter.getSessionDir or getSessionDBPath", () => {
|
||||
// Either directly uses adapter methods or a helper that delegates to them
|
||||
expect(serverSrc).toMatch(/getSessionDir\(\)|getSessionDBPath\(/);
|
||||
// ── Adapter methods used for session paths (post-C2 narrowing) ──
|
||||
test("session paths derived from adapter.getSessionDir + resolveSessionDbPath", () => {
|
||||
// C2 narrowing (2026-05): adapter no longer exposes getSessionDBPath /
|
||||
// getSessionEventsPath. server.ts must derive per-project DB paths via
|
||||
// resolveSessionDbPath while reading the adapter ONLY for the platform
|
||||
// sessionDir. Pin both calls so an accidental regression to a missing
|
||||
// helper or a deleted adapter method is caught at the test boundary.
|
||||
expect(serverSrc).toMatch(/getSessionDir\(/);
|
||||
expect(serverSrc).toMatch(/resolveSessionDbPath\(/);
|
||||
});
|
||||
|
||||
// ── Comprehensive projectDir detection ──
|
||||
|
||||
@@ -708,10 +708,10 @@ describe("ContextModePlugin", () => {
|
||||
);
|
||||
|
||||
// Verify SessionDB has the event
|
||||
const { SessionDB } = await import("../src/session/db.js");
|
||||
const { resolveSessionDbPath, SessionDB } = await import("../src/session/db.js");
|
||||
const { OpenCodeAdapter } = await import("../src/adapters/opencode/index.js");
|
||||
const adapter = new OpenCodeAdapter("opencode");
|
||||
const db = new SessionDB({ dbPath: adapter.getSessionDBPath(projectDir) });
|
||||
const db = new SessionDB({ dbPath: resolveSessionDbPath({ projectDir, sessionsDir: adapter.getSessionDir() }) });
|
||||
const events = db.getEvents("oc2-sess") as any[];
|
||||
db.close();
|
||||
const userPromptEvent = events.find((e: any) => e.type === "user_prompt");
|
||||
@@ -730,10 +730,10 @@ describe("ContextModePlugin", () => {
|
||||
{ message: { role: "user" } as any, parts: [{ type: "text", text: synthetic }] } as any,
|
||||
);
|
||||
|
||||
const { SessionDB } = await import("../src/session/db.js");
|
||||
const { resolveSessionDbPath, SessionDB } = await import("../src/session/db.js");
|
||||
const { OpenCodeAdapter } = await import("../src/adapters/opencode/index.js");
|
||||
const adapter = new OpenCodeAdapter("opencode");
|
||||
const db = new SessionDB({ dbPath: adapter.getSessionDBPath(projectDir) });
|
||||
const db = new SessionDB({ dbPath: resolveSessionDbPath({ projectDir, sessionsDir: adapter.getSessionDir() }) });
|
||||
const events = db.getEvents("oc2-skip") as any[];
|
||||
db.close();
|
||||
const userPromptEvent = events.find((e: any) => e.type === "user_prompt");
|
||||
@@ -807,10 +807,10 @@ describe("ContextModePlugin", () => {
|
||||
{ title: "Read", output: "x", metadata: {} },
|
||||
);
|
||||
|
||||
const { SessionDB } = await import("../src/session/db.js");
|
||||
const { resolveSessionDbPath, SessionDB } = await import("../src/session/db.js");
|
||||
const { OpenCodeAdapter } = await import("../src/adapters/opencode/index.js");
|
||||
const adapter = new OpenCodeAdapter("opencode");
|
||||
const db = new SessionDB({ dbPath: adapter.getSessionDBPath(projectDir) });
|
||||
const db = new SessionDB({ dbPath: resolveSessionDbPath({ projectDir, sessionsDir: adapter.getSessionDir() }) });
|
||||
const events = db.getEvents("oc4-sess-1") as any[];
|
||||
db.close();
|
||||
|
||||
@@ -839,10 +839,10 @@ describe("ContextModePlugin", () => {
|
||||
{ title: "Read", output: "b", metadata: {} },
|
||||
);
|
||||
|
||||
const { SessionDB } = await import("../src/session/db.js");
|
||||
const { resolveSessionDbPath, SessionDB } = await import("../src/session/db.js");
|
||||
const { OpenCodeAdapter } = await import("../src/adapters/opencode/index.js");
|
||||
const adapter = new OpenCodeAdapter("opencode");
|
||||
const db = new SessionDB({ dbPath: adapter.getSessionDBPath(projectDir) });
|
||||
const db = new SessionDB({ dbPath: resolveSessionDbPath({ projectDir, sessionsDir: adapter.getSessionDir() }) });
|
||||
const events = db.getEvents("oc4-idem") as any[];
|
||||
db.close();
|
||||
|
||||
@@ -868,10 +868,10 @@ describe("ContextModePlugin", () => {
|
||||
),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
const { SessionDB } = await import("../src/session/db.js");
|
||||
const { resolveSessionDbPath, SessionDB } = await import("../src/session/db.js");
|
||||
const { OpenCodeAdapter } = await import("../src/adapters/opencode/index.js");
|
||||
const adapter = new OpenCodeAdapter("opencode");
|
||||
const db = new SessionDB({ dbPath: adapter.getSessionDBPath(projectDir) });
|
||||
const db = new SessionDB({ dbPath: resolveSessionDbPath({ projectDir, sessionsDir: adapter.getSessionDir() }) });
|
||||
const events = db.getEvents("oc4-missing") as any[];
|
||||
db.close();
|
||||
|
||||
@@ -891,10 +891,10 @@ describe("ContextModePlugin", () => {
|
||||
{ title: "Read", output: "c", metadata: {} },
|
||||
);
|
||||
|
||||
const { SessionDB } = await import("../src/session/db.js");
|
||||
const { resolveSessionDbPath, SessionDB } = await import("../src/session/db.js");
|
||||
const { OpenCodeAdapter } = await import("../src/adapters/opencode/index.js");
|
||||
const adapter = new OpenCodeAdapter("opencode");
|
||||
const db = new SessionDB({ dbPath: adapter.getSessionDBPath(projectDir) });
|
||||
const db = new SessionDB({ dbPath: resolveSessionDbPath({ projectDir, sessionsDir: adapter.getSessionDir() }) });
|
||||
const events = db.getEvents("oc4-claude") as any[];
|
||||
db.close();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user