From 017528b497ac1c74e860aeffb50924a361e5456d Mon Sep 17 00:00:00 2001 From: chungyeong Date: Sun, 10 May 2026 01:27:43 +0900 Subject: [PATCH] feat: add fake session adapter --- packages/session/package.json | 17 + packages/session/src/adapter.ts | 41 +++ packages/session/src/fake.test.ts | 286 +++++++++++++++++ packages/session/src/fake.ts | 295 ++++++++++++++++++ packages/session/src/index.ts | 2 + packages/session/tsconfig.build.json | 10 + packages/session/tsconfig.json | 10 + pnpm-lock.yaml | 6 + .../fake-artifacts/dev/spec@1/ok.json | 11 + tsconfig.json | 1 + tsconfig.typecheck.json | 11 +- vitest.workspace.ts | 7 + 12 files changed, 695 insertions(+), 2 deletions(-) create mode 100644 packages/session/package.json create mode 100644 packages/session/src/adapter.ts create mode 100644 packages/session/src/fake.test.ts create mode 100644 packages/session/src/fake.ts create mode 100644 packages/session/src/index.ts create mode 100644 packages/session/tsconfig.build.json create mode 100644 packages/session/tsconfig.json create mode 100644 tests/fixtures/fake-artifacts/dev/spec@1/ok.json diff --git a/packages/session/package.json b/packages/session/package.json new file mode 100644 index 0000000..d4dbc16 --- /dev/null +++ b/packages/session/package.json @@ -0,0 +1,17 @@ +{ + "name": "@devflow/session", + "version": "0.0.0", + "private": true, + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "scripts": { + "build": "tsup src/index.ts --format esm,cjs --clean && tsc -p tsconfig.build.json", + "typecheck": "tsc -p ../../tsconfig.typecheck.json --noEmit", + "test": "vitest run" + }, + "dependencies": { + "@devflow/core": "workspace:*" + } +} diff --git a/packages/session/src/adapter.ts b/packages/session/src/adapter.ts new file mode 100644 index 0000000..cc216c6 --- /dev/null +++ b/packages/session/src/adapter.ts @@ -0,0 +1,41 @@ +import type { Backend, PromptEnvelope } from "@devflow/core"; + +export interface SessionAdapter { + start(input: StartInput): Promise; + sendPrompt(handle: SessionHandle, envelope: PromptEnvelope): Promise<{ promptId: string }>; + probe(handle: SessionHandle): Promise; + resume(handle: SessionHandle): Promise; + rebootstrap(handle: SessionHandle): Promise; + capture(handle: SessionHandle, fromSeq: bigint): AsyncIterable; + dispose(handle: SessionHandle): Promise; +} + +export interface StartInput { + runId: string; + roleId: string; + backend: Backend; + cwd: string; + expectedArtifactPath?: string; + expectedSchema?: string; + envelopePrelude?: string; +} + +export interface SessionHandle { + sessionId: string; + pid?: number; + tmuxSession?: string; + tmuxWindow?: string; +} + +export interface ProbeResult { + alive: boolean; + paneActive: boolean; + lastOutputAt?: Date; + hint?: string; +} + +export interface TranscriptChunk { + seq: bigint; + content: string; + capturedAt: Date; +} diff --git a/packages/session/src/fake.test.ts b/packages/session/src/fake.test.ts new file mode 100644 index 0000000..7385cb5 --- /dev/null +++ b/packages/session/src/fake.test.ts @@ -0,0 +1,286 @@ +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; + +import type { PromptEnvelope } from "@devflow/core"; +import { DevflowError } from "@devflow/core"; + +import { FakeSessionAdapter } from "./fake.js"; + +const runId = "00000000-0000-4000-8000-000000000001"; +const dedupKey = "a".repeat(64); +const secondDedupKey = "b".repeat(64); + +function envelope(overrides: Partial = {}): PromptEnvelope { + return { + uuid: "00000000-0000-4000-8000-000000000010", + runId, + roleId: "implementer", + phaseKey: "implement", + attempt: 0, + expectedArtifact: join(mkdtempSync(join(tmpdir(), "devflow-fake-artifact-")), "artifact.json"), + expectedSchema: "dev/spec@1", + dedupKey, + instructions: "Build the artifact", + ...overrides, + }; +} + +function makeFixtureRoot(): string { + const root = mkdtempSync(join(tmpdir(), "devflow-fake-fixtures-")); + const schemaDir = join(root, "dev", "spec@1"); + mkdirSync(schemaDir, { recursive: true }); + writeFileSync( + join(schemaDir, "ok.json"), + JSON.stringify({ + summary: "Fake spec", + requirements: [{ id: "REQ-1", description: "Write the file" }], + acceptanceCriteria: ["File is written"], + risks: [], + }), + ); + return root; +} + +async function waitForFile(path: string): Promise { + const deadline = Date.now() + 500; + while (Date.now() < deadline) { + if (existsSync(path)) { + return; + } + await new Promise((resolve) => setTimeout(resolve, 5)); + } + throw new Error(`Timed out waiting for ${path}`); +} + +async function collect(iterable: AsyncIterable): Promise { + const items: T[] = []; + for await (const item of iterable) { + items.push(item); + } + return items; +} + +describe("FakeSessionAdapter", () => { + const tempRoots: string[] = []; + + afterEach(() => { + for (const root of tempRoots.splice(0)) { + rmSync(root, { recursive: true, force: true }); + } + }); + + it("writes the ok fixture for the prompt schema and records transcript chunks", async () => { + const fixtureRoot = makeFixtureRoot(); + tempRoots.push(fixtureRoot); + const adapter = new FakeSessionAdapter({ fixtureRoot, writeDelayMs: 0 }); + const handle = await adapter.start({ + runId, + roleId: "implementer", + backend: "fake", + cwd: fixtureRoot, + }); + const prompt = envelope(); + tempRoots.push(join(prompt.expectedArtifact, "..")); + + await expect(adapter.sendPrompt(handle, prompt)).resolves.toEqual({ promptId: dedupKey }); + await waitForFile(prompt.expectedArtifact); + + expect(JSON.parse(readFileSync(prompt.expectedArtifact, "utf8"))).toMatchObject({ + summary: "Fake spec", + }); + const chunks = await collect(adapter.capture(handle, 0n)); + expect(chunks.map((chunk) => chunk.content).join("\n")).toContain( + `[fake] received prompt ${prompt.uuid}; will write ${prompt.expectedArtifact} in 0ms`, + ); + expect(chunks.every((chunk, index) => chunk.seq === BigInt(index + 1))).toBe(true); + }); + + it("refuses duplicate prompt dedup keys for the same session", async () => { + const fixtureRoot = makeFixtureRoot(); + tempRoots.push(fixtureRoot); + const adapter = new FakeSessionAdapter({ fixtureRoot, writeDelayMs: 0 }); + const handle = await adapter.start({ + runId, + roleId: "implementer", + backend: "fake", + cwd: fixtureRoot, + }); + const first = envelope(); + const duplicate = envelope({ + uuid: "00000000-0000-4000-8000-000000000011", + dedupKey, + }); + tempRoots.push(join(first.expectedArtifact, ".."), join(duplicate.expectedArtifact, "..")); + + await adapter.sendPrompt(handle, first); + + await expect(adapter.sendPrompt(handle, duplicate)).rejects.toMatchObject({ + code: "duplicate_prompt_dedup_key", + }); + await waitForFile(first.expectedArtifact); + }); + + it("preserves prompt dedup history across crash and rebootstrap recovery", async () => { + const fixtureRoot = makeFixtureRoot(); + tempRoots.push(fixtureRoot); + const adapter = new FakeSessionAdapter({ fixtureRoot, writeDelayMs: 0 }); + const handle = await adapter.start({ + runId, + roleId: "implementer", + backend: "fake", + cwd: fixtureRoot, + }); + const crash = envelope({ + dedupKey: "c".repeat(64), + instructions: "Scenario: crash\nCrash", + }); + + await expect(adapter.sendPrompt(handle, crash)).rejects.toMatchObject({ + code: "prompt_send_transient", + }); + await expect(adapter.sendPrompt(handle, crash)).rejects.toMatchObject({ + code: "duplicate_prompt_dedup_key", + }); + + const ok = envelope({ dedupKey: "d".repeat(64) }); + tempRoots.push(join(ok.expectedArtifact, "..")); + await adapter.sendPrompt(handle, ok); + await waitForFile(ok.expectedArtifact); + await adapter.rebootstrap(handle); + + await expect(adapter.sendPrompt(handle, ok)).rejects.toMatchObject({ + code: "duplicate_prompt_dedup_key", + }); + }); + + it("rejects prompts whose run or role do not match the session", async () => { + const fixtureRoot = makeFixtureRoot(); + tempRoots.push(fixtureRoot); + const adapter = new FakeSessionAdapter({ fixtureRoot, writeDelayMs: 0 }); + const handle = await adapter.start({ + runId, + roleId: "implementer", + backend: "fake", + cwd: fixtureRoot, + }); + + await expect( + adapter.sendPrompt( + handle, + envelope({ + runId: "00000000-0000-4000-8000-000000000099", + dedupKey: "e".repeat(64), + }), + ), + ).rejects.toMatchObject({ code: "prompt_session_mismatch" }); + await expect( + adapter.sendPrompt( + handle, + envelope({ + roleId: "reviewer", + dedupKey: "f".repeat(64), + }), + ), + ).rejects.toMatchObject({ code: "prompt_session_mismatch" }); + }); + + it("fails sendPrompt immediately when an ok fixture is missing", async () => { + const fixtureRoot = mkdtempSync(join(tmpdir(), "devflow-empty-fake-fixtures-")); + tempRoots.push(fixtureRoot); + const adapter = new FakeSessionAdapter({ fixtureRoot, writeDelayMs: 0 }); + const handle = await adapter.start({ + runId, + roleId: "implementer", + backend: "fake", + cwd: fixtureRoot, + }); + const prompt = envelope(); + tempRoots.push(join(prompt.expectedArtifact, "..")); + + await expect(adapter.sendPrompt(handle, prompt)).rejects.toMatchObject({ + class: "fatal", + code: "fake_fixture_missing", + }); + await new Promise((resolve) => setTimeout(resolve, 20)); + expect(existsSync(prompt.expectedArtifact)).toBe(false); + }); + + it("supports invalid, timeout, and crash sentinel scenarios", async () => { + const fixtureRoot = makeFixtureRoot(); + tempRoots.push(fixtureRoot); + const adapter = new FakeSessionAdapter({ fixtureRoot, writeDelayMs: 0 }); + const handle = await adapter.start({ + runId, + roleId: "implementer", + backend: "fake", + cwd: fixtureRoot, + }); + const invalid = envelope({ + dedupKey: secondDedupKey, + instructions: "Scenario: invalid\nBuild an invalid artifact", + }); + tempRoots.push(join(invalid.expectedArtifact, "..")); + + await adapter.sendPrompt(handle, invalid); + await waitForFile(invalid.expectedArtifact); + expect(JSON.parse(readFileSync(invalid.expectedArtifact, "utf8"))).toEqual({ + fake: "invalid", + }); + + const timeout = envelope({ + dedupKey: "c".repeat(64), + instructions: "Scenario: timeout\nDo not write", + }); + tempRoots.push(join(timeout.expectedArtifact, "..")); + await adapter.sendPrompt(handle, timeout); + await new Promise((resolve) => setTimeout(resolve, 20)); + expect(existsSync(timeout.expectedArtifact)).toBe(false); + + const crash = envelope({ + dedupKey: "d".repeat(64), + instructions: "Scenario: crash\nCrash", + }); + await expect(adapter.sendPrompt(handle, crash)).rejects.toBeInstanceOf(DevflowError); + await expect( + adapter.sendPrompt(handle, { + ...crash, + dedupKey: "e".repeat(64), + }), + ).rejects.toMatchObject({ + class: "recoverable", + code: "prompt_send_transient", + }); + }); + + it("probes, resumes, rebootstraps, captures from a sequence, and disposes sessions", async () => { + const fixtureRoot = makeFixtureRoot(); + tempRoots.push(fixtureRoot); + const adapter = new FakeSessionAdapter({ fixtureRoot, writeDelayMs: 0 }); + const handle = await adapter.start({ + runId, + roleId: "implementer", + backend: "fake", + cwd: fixtureRoot, + envelopePrelude: "Follow the fake protocol", + }); + + expect(await adapter.resume(handle)).toEqual(handle); + expect(await adapter.probe(handle)).toMatchObject({ alive: true, paneActive: true }); + + const rebootstrapped = await adapter.rebootstrap(handle); + expect(rebootstrapped.sessionId).toBe(handle.sessionId); + expect(await collect(adapter.capture(handle, 1n))).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + seq: 2n, + content: "[fake] rebootstrap complete", + }), + ]), + ); + + await adapter.dispose(handle); + expect(await adapter.probe(handle)).toMatchObject({ alive: false, paneActive: false }); + }); +}); diff --git a/packages/session/src/fake.ts b/packages/session/src/fake.ts new file mode 100644 index 0000000..fe9b2dc --- /dev/null +++ b/packages/session/src/fake.ts @@ -0,0 +1,295 @@ +import { randomUUID } from "node:crypto"; +import { + copyFileSync, + existsSync, + mkdirSync, + readFileSync, + statSync, + writeFileSync, +} from "node:fs"; +import { dirname, join, resolve } from "node:path"; + +import { DevflowError, type PromptEnvelope } from "@devflow/core"; + +import type { + ProbeResult, + SessionAdapter, + SessionHandle, + StartInput, + TranscriptChunk, +} from "./adapter.js"; + +export interface FakeSessionAdapterOptions { + fixtureRoot?: string; + writeDelayMs?: number; + sessionIdFactory?: () => string; + now?: () => Date; +} + +interface FakeSessionRecord { + handle: SessionHandle; + runId: string; + roleId: string; + alive: boolean; + disposed: boolean; + transcript: TranscriptChunk[]; + sentDedupKeys: Set; + timers: Set; + lastOutputAt?: Date; +} + +export class FakeSessionAdapter implements SessionAdapter { + private readonly fixtureRoot: string; + private readonly writeDelayMs: number; + private readonly sessionIdFactory: () => string; + private readonly now: () => Date; + private readonly sessions = new Map(); + + constructor(options: FakeSessionAdapterOptions = {}) { + this.fixtureRoot = options.fixtureRoot ?? defaultFixtureRoot(); + this.writeDelayMs = options.writeDelayMs ?? 50; + this.sessionIdFactory = options.sessionIdFactory ?? randomUUID; + this.now = options.now ?? (() => new Date()); + } + + async start(input: StartInput): Promise { + if (input.backend !== "fake") { + throw new DevflowError("FakeSessionAdapter only supports the fake backend", { + class: "fatal", + code: "backend_unavailable", + runId: input.runId, + }); + } + + const handle: SessionHandle = { sessionId: this.sessionIdFactory() }; + const record: FakeSessionRecord = { + handle, + runId: input.runId, + roleId: input.roleId, + alive: true, + disposed: false, + transcript: [], + sentDedupKeys: new Set(), + timers: new Set(), + }; + this.sessions.set(handle.sessionId, record); + this.appendTranscript(record, `[fake] session started for ${input.roleId} in ${input.cwd}`); + return handle; + } + + async sendPrompt(handle: SessionHandle, envelope: PromptEnvelope): Promise<{ promptId: string }> { + const record = this.requireLiveSession(handle); + if (envelope.runId !== record.runId || envelope.roleId !== record.roleId) { + throw new DevflowError("Prompt does not match fake session run or role", { + class: "fatal", + code: "prompt_session_mismatch", + runId: envelope.runId, + }); + } + if (record.sentDedupKeys.has(envelope.dedupKey)) { + throw new DevflowError("Duplicate prompt dedup key refused by fake session", { + class: "recoverable", + code: "duplicate_prompt_dedup_key", + runId: record.runId, + }); + } + + const scenarioName = scenarioFromInstructions(envelope.instructions); + record.sentDedupKeys.add(envelope.dedupKey); + + if (scenarioName === "crash") { + this.appendTranscript(record, `[fake] received prompt ${envelope.uuid}; crashing`); + throw new DevflowError("Fake session crash scenario", { + class: "recoverable", + code: "prompt_send_transient", + runId: envelope.runId, + }); + } + + if (scenarioName === "timeout") { + this.appendTranscript(record, `[fake] received prompt ${envelope.uuid}; timeout`); + return { promptId: envelope.dedupKey }; + } + + const fixturePath = + scenarioName === "invalid" + ? undefined + : resolveFixturePath( + this.fixtureRoot, + envelope.expectedSchema, + scenarioName, + envelope.runId, + ); + + this.appendTranscript( + record, + `[fake] received prompt ${envelope.uuid}; will write ${envelope.expectedArtifact} in ${this.writeDelayMs}ms`, + ); + const timer = setTimeout(() => { + record.timers.delete(timer); + if (!record.alive || record.disposed) { + return; + } + try { + if (scenarioName === "invalid") { + writeJsonArtifact(envelope.expectedArtifact, { fake: "invalid" }); + } else { + copyFixtureArtifact(fixturePath, envelope.expectedArtifact); + } + } catch (cause) { + record.alive = false; + this.appendTranscript( + record, + `[fake] failed to write artifact ${envelope.expectedArtifact}`, + ); + return; + } + this.appendTranscript(record, `[fake] wrote artifact ${envelope.expectedArtifact}`); + }, this.writeDelayMs); + record.timers.add(timer); + + return { promptId: envelope.dedupKey }; + } + + async probe(handle: SessionHandle): Promise { + const record = this.sessions.get(handle.sessionId); + if (record === undefined || !record.alive || record.disposed) { + return { alive: false, paneActive: false, hint: "fake session is not active" }; + } + const result: ProbeResult = { alive: true, paneActive: true }; + if (record.lastOutputAt !== undefined) { + return { ...result, lastOutputAt: record.lastOutputAt }; + } + return result; + } + + async resume(handle: SessionHandle): Promise { + return this.requireLiveSession(handle).handle; + } + + async rebootstrap(handle: SessionHandle): Promise { + const record = this.sessions.get(handle.sessionId); + if (record === undefined) { + throw new DevflowError("Cannot rebootstrap unknown fake session", { + class: "recoverable", + code: "pane_briefly_unresponsive", + }); + } + for (const timer of record.timers) { + clearTimeout(timer); + } + record.timers.clear(); + record.alive = true; + record.disposed = false; + this.appendTranscript(record, "[fake] rebootstrap complete"); + return record.handle; + } + + async *capture(handle: SessionHandle, fromSeq: bigint): AsyncIterable { + const record = this.sessions.get(handle.sessionId); + if (record === undefined) { + return; + } + for (const chunk of record.transcript) { + if (chunk.seq > fromSeq) { + yield chunk; + } + } + } + + async dispose(handle: SessionHandle): Promise { + const record = this.sessions.get(handle.sessionId); + if (record === undefined) { + return; + } + for (const timer of record.timers) { + clearTimeout(timer); + } + record.timers.clear(); + record.alive = false; + record.disposed = true; + } + + private requireLiveSession(handle: SessionHandle): FakeSessionRecord { + const record = this.sessions.get(handle.sessionId); + if (record === undefined || !record.alive || record.disposed) { + throw new DevflowError("Fake session is not active", { + class: "recoverable", + code: "pane_briefly_unresponsive", + }); + } + return record; + } + + private appendTranscript(record: FakeSessionRecord, content: string): void { + const capturedAt = this.now(); + record.lastOutputAt = capturedAt; + record.transcript.push({ + seq: BigInt(record.transcript.length + 1), + content, + capturedAt, + }); + } +} + +function scenarioFromInstructions(instructions: string): string { + const match = /^Scenario:\s*([A-Za-z0-9_-]+)\s*$/m.exec(instructions); + return match?.[1] ?? "ok"; +} + +function resolveFixturePath( + fixtureRoot: string, + expectedSchema: string, + scenarioName: string, + runId: string, +): string { + const fixturePath = join(fixtureRoot, expectedSchema, `${scenarioName}.json`); + if (!existsSync(fixturePath) || !statSync(fixturePath).isFile()) { + throw new DevflowError(`Missing fake artifact fixture ${fixturePath}`, { + class: "fatal", + code: "fake_fixture_missing", + runId, + }); + } + return fixturePath; +} + +function copyFixtureArtifact(fixturePath: string | undefined, expectedArtifact: string): void { + if (fixturePath === undefined) { + throw new DevflowError("Missing resolved fake artifact fixture path", { + class: "fatal", + code: "fake_fixture_missing", + }); + } + mkdirSync(dirname(expectedArtifact), { recursive: true }); + copyFileSync(fixturePath, expectedArtifact); +} + +function writeJsonArtifact(path: string, value: unknown): void { + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, JSON.stringify(value)); +} + +function defaultFixtureRoot(): string { + const workspaceRoot = findWorkspaceRoot(process.cwd()); + return join(workspaceRoot, "tests", "fixtures", "fake-artifacts"); +} + +function findWorkspaceRoot(start: string): string { + let current = resolve(start); + while (true) { + const packageJsonPath = join(current, "package.json"); + const workspacePath = join(current, "pnpm-workspace.yaml"); + if (existsSync(packageJsonPath) && existsSync(workspacePath)) { + const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")) as { name?: unknown }; + if (packageJson.name === "devflow") { + return current; + } + } + const parent = dirname(current); + if (parent === current) { + return resolve(start); + } + current = parent; + } +} diff --git a/packages/session/src/index.ts b/packages/session/src/index.ts new file mode 100644 index 0000000..644f756 --- /dev/null +++ b/packages/session/src/index.ts @@ -0,0 +1,2 @@ +export * from "./adapter.js"; +export * from "./fake.js"; diff --git a/packages/session/tsconfig.build.json b/packages/session/tsconfig.build.json new file mode 100644 index 0000000..28a70ef --- /dev/null +++ b/packages/session/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "composite": false, + "emitDeclarationOnly": true, + "noEmit": false + }, + "references": [], + "exclude": ["src/**/*.test.ts"] +} diff --git a/packages/session/tsconfig.json b/packages/session/tsconfig.json new file mode 100644 index 0000000..b9b6c01 --- /dev/null +++ b/packages/session/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "types": ["node", "vitest"] + }, + "include": ["src/**/*.ts"], + "references": [{ "path": "../core" }] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2a8dfc8..06ed763 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -97,6 +97,12 @@ importers: specifier: 8.20.0 version: 8.20.0 + packages/session: + dependencies: + '@devflow/core': + specifier: workspace:* + version: link:../core + packages: '@ampproject/remapping@2.3.0': diff --git a/tests/fixtures/fake-artifacts/dev/spec@1/ok.json b/tests/fixtures/fake-artifacts/dev/spec@1/ok.json new file mode 100644 index 0000000..e13be36 --- /dev/null +++ b/tests/fixtures/fake-artifacts/dev/spec@1/ok.json @@ -0,0 +1,11 @@ +{ + "summary": "Fake development specification", + "requirements": [ + { + "id": "REQ-1", + "description": "The fake adapter writes this deterministic fixture" + } + ], + "acceptanceCriteria": ["The expected artifact path contains this JSON document"], + "risks": [] +} diff --git a/tsconfig.json b/tsconfig.json index 8af8015..84f04cd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "references": [ { "path": "./packages/core" }, { "path": "./packages/db" }, + { "path": "./packages/session" }, { "path": "./apps/cli" } ] } diff --git a/tsconfig.typecheck.json b/tsconfig.typecheck.json index 266eacd..a15bf93 100644 --- a/tsconfig.typecheck.json +++ b/tsconfig.typecheck.json @@ -1,11 +1,18 @@ { "extends": "./tsconfig.base.json", "compilerOptions": { + "baseUrl": ".", "composite": false, "declaration": false, "declarationMap": false, "noEmit": true, - "types": ["node", "vitest"] + "types": ["node", "vitest"], + "paths": { + "@devflow/core": ["packages/core/src/index.ts"], + "@devflow/db": ["packages/db/src/index.ts"], + "@devflow/session": ["packages/session/src/index.ts"] + } }, - "include": ["apps/**/*.ts", "packages/**/*.ts", "scripts/**/*.ts", "tests/**/*.ts", "*.ts"] + "include": ["apps/**/*.ts", "packages/**/*.ts", "scripts/**/*.ts", "tests/**/*.ts", "*.ts"], + "exclude": ["**/dist/**"] } diff --git a/vitest.workspace.ts b/vitest.workspace.ts index b8011fc..04f2cd1 100644 --- a/vitest.workspace.ts +++ b/vitest.workspace.ts @@ -22,6 +22,13 @@ export default defineWorkspace([ environment: "node", }, }, + { + test: { + name: "packages/session", + include: ["packages/session/src/**/*.test.ts"], + environment: "node", + }, + }, { test: { name: "apps/cli",