Merge pull request 'feat(cli-uwf): thread read Content + step-details — #357' (#358) from feat/357-thread-read-content into main

This commit is contained in:
2026-05-19 06:58:24 +00:00
5 changed files with 550 additions and 79 deletions
+4 -1
View File
@@ -22,9 +22,12 @@
"yaml": "^2.8.4" "yaml": "^2.8.4"
}, },
"scripts": { "scripts": {
"test": "bun test" "test": "vitest run"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
},
"devDependencies": {
"vitest": "^4.1.6"
} }
} }
@@ -0,0 +1,391 @@
import { mkdir, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { bootstrap, putSchema } from "@uncaged/json-cas";
import { createFsStore } from "@uncaged/json-cas-fs";
import type { CasRef, ThreadId } from "@uncaged/uwf-protocol";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import {
cmdThreadRead,
cmdThreadStepDetails,
extractLastAssistantContent,
THREAD_READ_DEFAULT_QUOTA,
} from "../commands/thread.js";
import { registerUwfSchemas } from "../schemas.js";
import type { UwfStore } from "../store.js";
import { saveThreadsIndex } from "../store.js";
// ── schemas used in tests ────────────────────────────────────────────────────
const TURN_SCHEMA = {
title: "hermes-turn",
type: "object" as const,
required: ["index", "role", "content"],
properties: {
index: { type: "integer" as const },
role: { type: "string" as const },
content: { type: "string" as const },
toolCalls: {
anyOf: [
{ type: "array" as const, items: { type: "object" as const } },
{ type: "null" as const },
],
},
reasoning: { anyOf: [{ type: "string" as const }, { type: "null" as const }] },
},
additionalProperties: false,
};
const DETAIL_SCHEMA = {
title: "hermes-detail",
type: "object" as const,
required: ["sessionId", "model", "duration", "turnCount", "turns"],
properties: {
sessionId: { type: "string" as const },
model: { type: "string" as const },
duration: { type: "integer" as const },
turnCount: { type: "integer" as const },
turns: {
type: "array" as const,
items: { type: "string" as const, format: "cas_ref" },
},
},
additionalProperties: false,
};
// ── helpers ───────────────────────────────────────────────────────────────────
async function makeUwfStore(storageRoot: string): Promise<UwfStore> {
const casDir = join(storageRoot, "cas");
await mkdir(casDir, { recursive: true });
const store = createFsStore(casDir);
const schemas = await registerUwfSchemas(store);
return { storageRoot, store, schemas };
}
async function registerDetailSchemas(store: ReturnType<typeof createFsStore>) {
await bootstrap(store);
const [turn, detail] = await Promise.all([
putSchema(store, TURN_SCHEMA),
putSchema(store, DETAIL_SCHEMA),
]);
return { turn, detail };
}
// ── fixture ───────────────────────────────────────────────────────────────────
let tmpDir: string;
beforeEach(async () => {
tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-test-"));
});
afterEach(async () => {
await rm(tmpDir, { recursive: true, force: true });
});
// ── extractLastAssistantContent ───────────────────────────────────────────────
describe("extractLastAssistantContent", () => {
test("returns last non-empty assistant content from turns", async () => {
const uwf = await makeUwfStore(tmpDir);
const schemas = await registerDetailSchemas(uwf.store);
const turn1 = await uwf.store.put(schemas.turn, {
index: 0,
role: "assistant",
content: "intermediate",
toolCalls: null,
reasoning: null,
});
const turn2 = await uwf.store.put(schemas.turn, {
index: 1,
role: "tool",
content: "ok",
toolCalls: null,
reasoning: null,
});
const turn3 = await uwf.store.put(schemas.turn, {
index: 2,
role: "assistant",
content: "final answer",
toolCalls: null,
reasoning: null,
});
const detailHash = await uwf.store.put(schemas.detail, {
sessionId: "s1",
model: "m1",
duration: 1000,
turnCount: 3,
turns: [turn1, turn2, turn3],
});
expect(extractLastAssistantContent(uwf, detailHash)).toBe("final answer");
});
test("returns null when detail node does not exist in store", async () => {
const uwf = await makeUwfStore(tmpDir);
expect(extractLastAssistantContent(uwf, "nonexistent00" as CasRef)).toBeNull();
});
test("returns null when turns array is empty", async () => {
const uwf = await makeUwfStore(tmpDir);
const schemas = await registerDetailSchemas(uwf.store);
const detailHash = await uwf.store.put(schemas.detail, {
sessionId: "s2",
model: "m2",
duration: 0,
turnCount: 0,
turns: [],
});
expect(extractLastAssistantContent(uwf, detailHash)).toBeNull();
});
test("returns null when all assistant turns have empty content", async () => {
const uwf = await makeUwfStore(tmpDir);
const schemas = await registerDetailSchemas(uwf.store);
const turn1 = await uwf.store.put(schemas.turn, {
index: 0,
role: "assistant",
content: "",
toolCalls: null,
reasoning: null,
});
const detailHash = await uwf.store.put(schemas.detail, {
sessionId: "s3",
model: "m3",
duration: 0,
turnCount: 1,
turns: [turn1],
});
expect(extractLastAssistantContent(uwf, detailHash)).toBeNull();
});
test("skips whitespace-only assistant content and returns earlier match", async () => {
const uwf = await makeUwfStore(tmpDir);
const schemas = await registerDetailSchemas(uwf.store);
const turn1 = await uwf.store.put(schemas.turn, {
index: 0,
role: "assistant",
content: "real content",
toolCalls: null,
reasoning: null,
});
const turn2 = await uwf.store.put(schemas.turn, {
index: 1,
role: "assistant",
content: " ",
toolCalls: null,
reasoning: null,
});
const detailHash = await uwf.store.put(schemas.detail, {
sessionId: "s4",
model: "m4",
duration: 0,
turnCount: 2,
turns: [turn1, turn2],
});
expect(extractLastAssistantContent(uwf, detailHash)).toBe("real content");
});
});
// ── cmdThreadRead: ### Content section ───────────────────────────────────────
describe("cmdThreadRead ### Content section", () => {
test("includes ### Content before ### Output when detail has assistant turns", async () => {
const uwf = await makeUwfStore(tmpDir);
const detailSchemas = await registerDetailSchemas(uwf.store);
const workflowHash = await uwf.store.put(uwf.schemas.workflow, {
name: "test-wf",
description: "desc",
roles: {
writer: {
description: "Write",
systemPrompt: "You are a writer.",
outputSchema: "placeholder00" as CasRef,
},
},
conditions: {},
graph: {},
});
const startHash = await uwf.store.put(uwf.schemas.startNode, {
workflow: workflowHash,
prompt: "Write something",
});
const outputHash = await uwf.store.put(uwf.schemas.workflow, {
name: "out",
description: "",
roles: {},
conditions: {},
graph: {},
});
const turnHash = await uwf.store.put(detailSchemas.turn, {
index: 0,
role: "assistant",
content: "The assistant response text",
toolCalls: null,
reasoning: null,
});
const detailHash = await uwf.store.put(detailSchemas.detail, {
sessionId: "sx",
model: "mx",
duration: 500,
turnCount: 1,
turns: [turnHash],
});
const stepHash = await uwf.store.put(uwf.schemas.stepNode, {
start: startHash,
prev: null,
role: "writer",
output: outputHash,
detail: detailHash,
agent: "uwf-hermes",
});
const threadId = "01JTEST0000000000000000001" as ThreadId;
await saveThreadsIndex(tmpDir, { [threadId]: stepHash });
const markdown = await cmdThreadRead(tmpDir, threadId, THREAD_READ_DEFAULT_QUOTA, null, false);
expect(markdown).toContain("### Content");
expect(markdown).toContain("The assistant response text");
const contentIdx = markdown.indexOf("### Content");
const outputIdx = markdown.indexOf("### Output");
expect(contentIdx).toBeGreaterThanOrEqual(0);
expect(outputIdx).toBeGreaterThanOrEqual(0);
expect(contentIdx).toBeLessThan(outputIdx);
});
test("omits ### Content when detail has no matching assistant turns", async () => {
const uwf = await makeUwfStore(tmpDir);
const workflowHash = await uwf.store.put(uwf.schemas.workflow, {
name: "test-wf2",
description: "desc",
roles: {},
conditions: {},
graph: {},
});
const startHash = await uwf.store.put(uwf.schemas.startNode, {
workflow: workflowHash,
prompt: "Do stuff",
});
const outputHash = await uwf.store.put(uwf.schemas.workflow, {
name: "out",
description: "",
roles: {},
conditions: {},
graph: {},
});
// A detail ref that doesn't exist in the store → extractLastAssistantContent returns null
const missingDetailRef = "missingdetail0" as CasRef;
const stepHash = await uwf.store.put(uwf.schemas.stepNode, {
start: startHash,
prev: null,
role: "worker",
output: outputHash,
detail: missingDetailRef,
agent: "uwf-hermes",
});
const threadId = "01JTEST0000000000000000002" as ThreadId;
await saveThreadsIndex(tmpDir, { [threadId]: stepHash });
const markdown = await cmdThreadRead(tmpDir, threadId, THREAD_READ_DEFAULT_QUOTA, null, false);
expect(markdown).not.toContain("### Content");
expect(markdown).toContain("### Output");
});
});
// ── cmdThreadStepDetails ──────────────────────────────────────────────────────
describe("cmdThreadStepDetails", () => {
test("returns expanded detail node with turns inlined", async () => {
const uwf = await makeUwfStore(tmpDir);
const detailSchemas = await registerDetailSchemas(uwf.store);
const workflowHash = await uwf.store.put(uwf.schemas.workflow, {
name: "wf",
description: "",
roles: {},
conditions: {},
graph: {},
});
const startHash = await uwf.store.put(uwf.schemas.startNode, {
workflow: workflowHash,
prompt: "p",
});
const outputHash = await uwf.store.put(uwf.schemas.workflow, {
name: "out",
description: "",
roles: {},
conditions: {},
graph: {},
});
const turnHash = await uwf.store.put(detailSchemas.turn, {
index: 0,
role: "assistant",
content: "done",
toolCalls: null,
reasoning: null,
});
const detailHash = await uwf.store.put(detailSchemas.detail, {
sessionId: "sess42",
model: "gpt-4o",
duration: 3000,
turnCount: 1,
turns: [turnHash],
});
const stepHash = await uwf.store.put(uwf.schemas.stepNode, {
start: startHash,
prev: null,
role: "coder",
output: outputHash,
detail: detailHash,
agent: "uwf-hermes",
});
const result = await cmdThreadStepDetails(tmpDir, stepHash);
expect(result).toMatchObject({
sessionId: "sess42",
model: "gpt-4o",
duration: 3000,
turnCount: 1,
});
const expanded = result as Record<string, unknown>;
expect(Array.isArray(expanded.turns)).toBe(true);
const turns = expanded.turns as unknown[];
expect(turns).toHaveLength(1);
expect(turns[0]).toMatchObject({
index: 0,
role: "assistant",
content: "done",
});
});
test("throws when step hash does not exist", async () => {
await expect(cmdThreadStepDetails(tmpDir, "nonexistenth0" as CasRef)).rejects.toThrow();
});
});
+79 -57
View File
@@ -1,33 +1,34 @@
#!/usr/bin/env bun #!/usr/bin/env bun
import { Command } from "commander";
import type { ThreadId } from "@uncaged/uwf-protocol"; import type { ThreadId } from "@uncaged/uwf-protocol";
import { Command } from "commander";
import { stringify as yamlStringify } from "yaml";
import {
cmdCasGet,
cmdCasHas,
cmdCasPut,
cmdCasRefs,
cmdCasReindex,
cmdCasSchemaGet,
cmdCasSchemaList,
cmdCasWalk,
} from "./commands/cas.js";
import { cmdSetup, cmdSetupInteractive } from "./commands/setup.js";
import { import {
cmdThreadFork, cmdThreadFork,
cmdThreadKill, cmdThreadKill,
cmdThreadList, cmdThreadList,
cmdThreadRead, cmdThreadRead,
THREAD_READ_DEFAULT_QUOTA,
cmdThreadShow, cmdThreadShow,
cmdThreadStart, cmdThreadStart,
cmdThreadStep, cmdThreadStep,
cmdThreadStepDetails,
cmdThreadSteps, cmdThreadSteps,
THREAD_READ_DEFAULT_QUOTA,
} from "./commands/thread.js"; } from "./commands/thread.js";
import { cmdWorkflowList, cmdWorkflowPut, cmdWorkflowShow } from "./commands/workflow.js"; import { cmdWorkflowList, cmdWorkflowPut, cmdWorkflowShow } from "./commands/workflow.js";
import { cmdSetup, cmdSetupInteractive } from "./commands/setup.js"; import { formatOutput, type OutputFormat } from "./format.js";
import {
cmdCasGet,
cmdCasHas,
cmdCasPut,
cmdCasReindex,
cmdCasRefs,
cmdCasSchemaGet,
cmdCasSchemaList,
cmdCasWalk,
} from "./commands/cas.js";
import { resolveStorageRoot } from "./store.js"; import { resolveStorageRoot } from "./store.js";
import { type OutputFormat, formatOutput } from "./format.js";
function writeOutput(data: unknown): void { function writeOutput(data: unknown): void {
const fmt = program.opts().format as OutputFormat; const fmt = program.opts().format as OutputFormat;
@@ -168,20 +169,27 @@ thread
.option("--quota <chars>", "Max output characters", String(THREAD_READ_DEFAULT_QUOTA)) .option("--quota <chars>", "Max output characters", String(THREAD_READ_DEFAULT_QUOTA))
.option("--before <step-hash>", "Load steps before this hash (exclusive)") .option("--before <step-hash>", "Load steps before this hash (exclusive)")
.option("--start", "Include start step in output") .option("--start", "Include start step in output")
.option("--detail", "Expand detail content for each step") .action(
.action((threadId: string, opts: { quota: string; before: string | undefined; start: boolean; detail: boolean }) => { (threadId: string, opts: { quota: string; before: string | undefined; start: boolean }) => {
const storageRoot = resolveStorageRoot(); const storageRoot = resolveStorageRoot();
runAction(async () => { runAction(async () => {
const quota = Number.parseInt(opts.quota, 10); const quota = Number.parseInt(opts.quota, 10);
if (!Number.isFinite(quota) || quota < 1) { if (!Number.isFinite(quota) || quota < 1) {
process.stderr.write("invalid --quota: must be a positive integer\n"); process.stderr.write("invalid --quota: must be a positive integer\n");
process.exit(1); process.exit(1);
} }
const before = opts.before ?? null; const before = opts.before ?? null;
const markdown = await cmdThreadRead(storageRoot, threadId as ThreadId, quota, before, opts.start ?? false, opts.detail ?? false); const markdown = await cmdThreadRead(
process.stdout.write(markdown.endsWith("\n") ? markdown : `${markdown}\n`); storageRoot,
}); threadId as ThreadId,
}); quota,
before,
opts.start ?? false,
);
process.stdout.write(markdown.endsWith("\n") ? markdown : `${markdown}\n`);
});
},
);
thread thread
.command("fork") .command("fork")
@@ -195,6 +203,18 @@ thread
}); });
}); });
thread
.command("step-details")
.description("Dump the full detail node of a step as YAML")
.argument("<step-hash>", "CAS hash of the StepNode")
.action((stepHash: string) => {
const storageRoot = resolveStorageRoot();
runAction(async () => {
const detail = await cmdThreadStepDetails(storageRoot, stepHash);
process.stdout.write(yamlStringify(detail));
});
});
program program
.command("setup") .command("setup")
.description("Configure provider, model, and agent") .description("Configure provider, model, and agent")
@@ -203,34 +223,36 @@ program
.option("--api-key <key>", "API key") .option("--api-key <key>", "API key")
.option("--model <name>", "Default model name") .option("--model <name>", "Default model name")
.option("--agent <name>", "Default agent alias") .option("--agent <name>", "Default agent alias")
.action((opts: { .action(
provider?: string; (opts: {
baseUrl?: string; provider?: string;
apiKey?: string; baseUrl?: string;
model?: string; apiKey?: string;
agent?: string; model?: string;
}) => { agent?: string;
const storageRoot = resolveStorageRoot(); }) => {
runAction(async () => { const storageRoot = resolveStorageRoot();
if (opts.provider && opts.baseUrl && opts.apiKey && opts.model) { runAction(async () => {
const result = await cmdSetup({ if (opts.provider && opts.baseUrl && opts.apiKey && opts.model) {
provider: opts.provider, const result = await cmdSetup({
baseUrl: opts.baseUrl, provider: opts.provider,
apiKey: opts.apiKey, baseUrl: opts.baseUrl,
model: opts.model, apiKey: opts.apiKey,
agent: opts.agent ?? undefined, model: opts.model,
storageRoot, agent: opts.agent ?? undefined,
}); storageRoot,
writeOutput(result); });
} else if (!opts.provider && !opts.baseUrl && !opts.apiKey && !opts.model) { writeOutput(result);
await cmdSetupInteractive(storageRoot); } else if (!opts.provider && !opts.baseUrl && !opts.apiKey && !opts.model) {
} else { await cmdSetupInteractive(storageRoot);
throw new Error( } else {
"Non-interactive setup requires all of: --provider, --base-url, --api-key, --model", throw new Error(
); "Non-interactive setup requires all of: --provider, --base-url, --api-key, --model",
} );
}); }
}); });
},
);
const cas = program.command("cas").description("Content-addressable storage operations"); const cas = program.command("cas").description("Content-addressable storage operations");
+69 -21
View File
@@ -1,8 +1,6 @@
import { execFileSync } from "node:child_process"; import { execFileSync } from "node:child_process";
import type { Store as CasStore, JSONSchema } from "@uncaged/json-cas";
import { getSchema, validate } from "@uncaged/json-cas"; import { getSchema, validate } from "@uncaged/json-cas";
import type { JSONSchema, Store as CasStore } from "@uncaged/json-cas";
import { stringify } from "yaml";
import { getEnvPath, loadWorkflowConfig } from "@uncaged/uwf-agent-kit"; import { getEnvPath, loadWorkflowConfig } from "@uncaged/uwf-agent-kit";
import { evaluate } from "@uncaged/uwf-moderator"; import { evaluate } from "@uncaged/uwf-moderator";
import type { import type {
@@ -26,6 +24,7 @@ import type {
} from "@uncaged/uwf-protocol"; } from "@uncaged/uwf-protocol";
import { generateUlid } from "@uncaged/workflow-util"; import { generateUlid } from "@uncaged/workflow-util";
import { config as loadDotenv } from "dotenv"; import { config as loadDotenv } from "dotenv";
import { stringify } from "yaml";
import { import {
appendThreadHistory, appendThreadHistory,
@@ -293,7 +292,12 @@ function expandDeep(store: CasStore, hash: CasRef, visited?: Set<string>): unkno
return expandValue(store, schema, node.payload, seen); return expandValue(store, schema, node.payload, seen);
} }
function expandValue(store: CasStore, schema: JSONSchema, value: unknown, visited: Set<string>): unknown { function expandValue(
store: CasStore,
schema: JSONSchema,
value: unknown,
visited: Set<string>,
): unknown {
// If this field is a cas_ref, expand it // If this field is a cas_ref, expand it
if (schema.format === "cas_ref") { if (schema.format === "cas_ref") {
if (typeof value === "string") { if (typeof value === "string") {
@@ -383,6 +387,37 @@ function formatCompactStep(index: number, item: OrderedStepItem, outputYaml: str
].join("\n"); ].join("\n");
} }
export function extractLastAssistantContent(uwf: UwfStore, detailRef: CasRef): string | null {
const detailNode = uwf.store.get(detailRef);
if (detailNode === null) {
return null;
}
const detail = detailNode.payload as Record<string, unknown>;
const turns = detail.turns;
if (!Array.isArray(turns) || turns.length === 0) {
return null;
}
for (let i = turns.length - 1; i >= 0; i--) {
const turnRef = turns[i];
if (typeof turnRef !== "string") {
continue;
}
const turnNode = uwf.store.get(turnRef as CasRef);
if (turnNode === null) {
continue;
}
const turn = turnNode.payload as Record<string, unknown>;
if (
turn.role === "assistant" &&
typeof turn.content === "string" &&
turn.content.trim() !== ""
) {
return turn.content;
}
}
return null;
}
function formatThreadReadMarkdown(options: { function formatThreadReadMarkdown(options: {
threadId: ThreadId; threadId: ThreadId;
workflowName: string; workflowName: string;
@@ -394,9 +429,8 @@ function formatThreadReadMarkdown(options: {
quota: number; quota: number;
before: CasRef | null; before: CasRef | null;
showStart: boolean; showStart: boolean;
showDetail: boolean;
}): string { }): string {
const { ordered, uwf, workflow, quota, before, showStart, showDetail } = options; const { ordered, uwf, workflow, quota, before, showStart } = options;
// Determine which steps to consider // Determine which steps to consider
let candidates = ordered; let candidates = ordered;
@@ -456,7 +490,10 @@ function formatThreadReadMarkdown(options: {
if (item === undefined) continue; if (item === undefined) continue;
const stepNum = startIndex + i + 1; const stepNum = startIndex + i + 1;
const outputYaml = formatYaml(expandOutput(uwf, item.payload.output)); const outputYaml = formatYaml(expandOutput(uwf, item.payload.output));
const ts = new Date(item.timestamp).toISOString().replace("T", " ").replace(/\.\d+Z$/, ""); const ts = new Date(item.timestamp)
.toISOString()
.replace("T", " ")
.replace(/\.\d+Z$/, "");
const stepLines = [ const stepLines = [
`## Step ${stepNum}: ${item.payload.role} \`${item.hash}\``, `## Step ${stepNum}: ${item.payload.role} \`${item.hash}\``,
`**Agent:** ${item.payload.agent} | **Time:** ${ts}`, `**Agent:** ${item.payload.agent} | **Time:** ${ts}`,
@@ -465,19 +502,13 @@ function formatThreadReadMarkdown(options: {
if (roleDef) { if (roleDef) {
stepLines.push("", "### Prompt", "", roleDef.systemPrompt); stepLines.push("", "### Prompt", "", roleDef.systemPrompt);
} }
stepLines.push( if (item.payload.detail) {
"", const content = extractLastAssistantContent(uwf, item.payload.detail);
"### Output", if (content !== null) {
"", stepLines.push("", "### Content", "", content);
"```yaml", }
outputYaml,
"```",
);
if (showDetail && item.payload.detail) {
const detailExpanded = expandDeep(uwf.store, item.payload.detail);
const detailYaml = formatYaml(detailExpanded);
stepLines.push("", "### Detail", "", "```yaml", detailYaml, "```");
} }
stepLines.push("", "### Output", "", "```yaml", outputYaml, "```");
parts.push(stepLines.join("\n")); parts.push(stepLines.join("\n"));
} }
@@ -719,7 +750,6 @@ export async function cmdThreadRead(
quota: number = THREAD_READ_DEFAULT_QUOTA, quota: number = THREAD_READ_DEFAULT_QUOTA,
before: CasRef | null = null, before: CasRef | null = null,
showStart: boolean = false, showStart: boolean = false,
showDetail: boolean = false,
): Promise<string> { ): Promise<string> {
const headHash = await resolveHeadHash(storageRoot, threadId); const headHash = await resolveHeadHash(storageRoot, threadId);
const uwf = await createUwfStore(storageRoot); const uwf = await createUwfStore(storageRoot);
@@ -738,7 +768,6 @@ export async function cmdThreadRead(
quota, quota,
before, before,
showStart, showStart,
showDetail,
}); });
} }
@@ -768,6 +797,25 @@ export async function cmdThreadFork(
}; };
} }
export async function cmdThreadStepDetails(
storageRoot: string,
stepHash: CasRef,
): Promise<unknown> {
const uwf = await createUwfStore(storageRoot);
const node = uwf.store.get(stepHash);
if (node === null) {
fail(`CAS node not found: ${stepHash}`);
}
if (node.type !== uwf.schemas.stepNode) {
fail(`node ${stepHash} is not a StepNode`);
}
const payload = node.payload as StepNodePayload;
if (!payload.detail) {
fail(`step ${stepHash} has no detail`);
}
return expandDeep(uwf.store, payload.detail);
}
export async function cmdThreadKill(storageRoot: string, threadId: ThreadId): Promise<KillOutput> { export async function cmdThreadKill(storageRoot: string, threadId: ThreadId): Promise<KillOutput> {
const index = await loadThreadsIndex(storageRoot); const index = await loadThreadsIndex(storageRoot);
const head = index[threadId]; const head = index[threadId];
+7
View File
@@ -0,0 +1,7 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
include: ["src/__tests__/**/*.test.ts"],
},
});