feat(HAI-017): add per-task write lock, atomic writes, and defensive JSON parsing to TaskStore
- Add per-task write lock in TaskStore to prevent concurrent write corruption - Implement defensive JSON parsing with recovery for task.json files - Use atomic writes (write-to-temp + rename) for task.json updates - Add vitest config and tests for lock, parsing, and atomic write behavior - Remove concurrency module from engine, consolidating store safety in core
This commit is contained in:
@@ -7,10 +7,12 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit",
|
||||||
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^25.5.0",
|
"@types/node": "^25.5.0",
|
||||||
"typescript": "^5.7.0"
|
"typescript": "^5.7.0",
|
||||||
|
"vitest": "^3.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
158
packages/core/src/store.test.ts
Normal file
158
packages/core/src/store.test.ts
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||||
|
import { TaskStore } from "./store.js";
|
||||||
|
import { readFile, writeFile, mkdir, rm, readdir } from "node:fs/promises";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { mkdtempSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import type { Task } from "./types.js";
|
||||||
|
|
||||||
|
function makeTmpDir(): string {
|
||||||
|
return mkdtempSync(join(tmpdir(), "hai-store-test-"));
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("TaskStore", () => {
|
||||||
|
let rootDir: string;
|
||||||
|
let store: TaskStore;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
rootDir = makeTmpDir();
|
||||||
|
store = new TaskStore(rootDir);
|
||||||
|
await store.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
store.stopWatching();
|
||||||
|
await rm(rootDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
async function createTestTask(): Promise<Task> {
|
||||||
|
return store.createTask({ description: "Test task" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createTaskWithSteps(): Promise<Task> {
|
||||||
|
const task = await store.createTask({ description: "Task with steps" });
|
||||||
|
// Write a PROMPT.md with steps so updateStep works
|
||||||
|
const dir = join(rootDir, ".hai", "tasks", task.id);
|
||||||
|
await writeFile(
|
||||||
|
join(dir, "PROMPT.md"),
|
||||||
|
`# ${task.id}: Task with steps
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### Step 0: Preflight
|
||||||
|
|
||||||
|
- [ ] Check things
|
||||||
|
|
||||||
|
### Step 1: Implementation
|
||||||
|
|
||||||
|
- [ ] Do stuff
|
||||||
|
|
||||||
|
### Step 2: Testing
|
||||||
|
|
||||||
|
- [ ] Test stuff
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Lock serialization test ──────────────────────────────────────
|
||||||
|
|
||||||
|
describe("write lock serialization", () => {
|
||||||
|
it("serializes concurrent logEntry and updateStep calls without corruption", async () => {
|
||||||
|
const task = await createTaskWithSteps();
|
||||||
|
const id = task.id;
|
||||||
|
|
||||||
|
// Fire 20 concurrent operations: 10 logEntry + 10 updateStep (alternating steps)
|
||||||
|
const promises: Promise<Task>[] = [];
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
if (i % 2 === 0) {
|
||||||
|
promises.push(store.logEntry(id, `Log entry ${i}`));
|
||||||
|
} else {
|
||||||
|
// Toggle step 0 between in-progress and done
|
||||||
|
const status = i % 4 === 1 ? "in-progress" : "done";
|
||||||
|
promises.push(store.updateStep(id, 0, status));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
|
// Read back and verify valid JSON
|
||||||
|
const taskJsonPath = join(rootDir, ".hai", "tasks", id, "task.json");
|
||||||
|
const raw = await readFile(taskJsonPath, "utf-8");
|
||||||
|
const result = JSON.parse(raw) as Task;
|
||||||
|
|
||||||
|
// Check all 10 log entries are present (plus initial "Task created" + step update logs)
|
||||||
|
const customLogs = result.log.filter((l) => l.action.startsWith("Log entry"));
|
||||||
|
expect(customLogs).toHaveLength(10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Defensive parsing test ───────────────────────────────────────
|
||||||
|
|
||||||
|
describe("defensive JSON parsing", () => {
|
||||||
|
it("recovers from corrupted task.json with trailing duplicate content", async () => {
|
||||||
|
const task = await createTestTask();
|
||||||
|
const taskJsonPath = join(rootDir, ".hai", "tasks", task.id, "task.json");
|
||||||
|
|
||||||
|
// Corrupt the file: append duplicate trailing content (like HAI-015)
|
||||||
|
const validJson = await readFile(taskJsonPath, "utf-8");
|
||||||
|
const corrupted = validJson + validJson.slice(validJson.length / 2);
|
||||||
|
await writeFile(taskJsonPath, corrupted);
|
||||||
|
|
||||||
|
// getTask should recover
|
||||||
|
const recovered = await store.getTask(task.id);
|
||||||
|
expect(recovered.id).toBe(task.id);
|
||||||
|
expect(recovered.description).toBe("Test task");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws a clear error when JSON is completely unrecoverable", async () => {
|
||||||
|
const task = await createTestTask();
|
||||||
|
const taskJsonPath = join(rootDir, ".hai", "tasks", task.id, "task.json");
|
||||||
|
|
||||||
|
// Write completely invalid content
|
||||||
|
await writeFile(taskJsonPath, "not json at all {{{");
|
||||||
|
|
||||||
|
await expect(store.getTask(task.id)).rejects.toThrow("Failed to parse task.json");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Atomic write test ────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("atomic writes", () => {
|
||||||
|
it("produces valid JSON after write with no .tmp files left behind", async () => {
|
||||||
|
const task = await createTestTask();
|
||||||
|
const dir = join(rootDir, ".hai", "tasks", task.id);
|
||||||
|
|
||||||
|
// Perform a write
|
||||||
|
await store.logEntry(task.id, "atomic test");
|
||||||
|
|
||||||
|
// Verify valid JSON
|
||||||
|
const raw = await readFile(join(dir, "task.json"), "utf-8");
|
||||||
|
const parsed = JSON.parse(raw) as Task;
|
||||||
|
expect(parsed.log.some((l) => l.action === "atomic test")).toBe(true);
|
||||||
|
|
||||||
|
// Verify no .tmp files
|
||||||
|
const files = await readdir(dir);
|
||||||
|
expect(files.filter((f) => f.endsWith(".tmp"))).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Concurrent stress test ───────────────────────────────────────
|
||||||
|
|
||||||
|
describe("concurrent stress", () => {
|
||||||
|
it("handles 10 parallel logEntry calls preserving all entries", async () => {
|
||||||
|
const task = await createTestTask();
|
||||||
|
const initialLogCount = task.log.length; // 1 ("Task created")
|
||||||
|
|
||||||
|
const promises = Array.from({ length: 10 }, (_, i) =>
|
||||||
|
store.logEntry(task.id, `Stress log ${i}`),
|
||||||
|
);
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
|
const result = await store.getTask(task.id);
|
||||||
|
const stressLogs = result.log.filter((l) => l.action.startsWith("Stress log"));
|
||||||
|
expect(stressLogs).toHaveLength(10);
|
||||||
|
expect(result.log).toHaveLength(initialLogCount + 10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { EventEmitter } from "node:events";
|
import { EventEmitter } from "node:events";
|
||||||
import { execSync } from "node:child_process";
|
import { execSync } from "node:child_process";
|
||||||
import { mkdir, readFile, writeFile, readdir } from "node:fs/promises";
|
import { mkdir, readFile, writeFile, readdir, rename } from "node:fs/promises";
|
||||||
import { join, sep } from "node:path";
|
import { join, sep } from "node:path";
|
||||||
import { existsSync, watch, type FSWatcher } from "node:fs";
|
import { existsSync, watch, type FSWatcher } from "node:fs";
|
||||||
import type { Task, TaskDetail, TaskCreateInput, BoardConfig, Column, MergeResult, Settings } from "./types.js";
|
import type { Task, TaskDetail, TaskCreateInput, BoardConfig, Column, MergeResult, Settings } from "./types.js";
|
||||||
@@ -29,6 +29,8 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
|||||||
private debounceTimers: Map<string, ReturnType<typeof setTimeout>> = new Map();
|
private debounceTimers: Map<string, ReturnType<typeof setTimeout>> = new Map();
|
||||||
/** Debounce interval in ms */
|
/** Debounce interval in ms */
|
||||||
private debounceMs = 150;
|
private debounceMs = 150;
|
||||||
|
/** Per-task promise chain for serializing writes */
|
||||||
|
private taskLocks: Map<string, Promise<void>> = new Map();
|
||||||
|
|
||||||
constructor(private rootDir: string) {
|
constructor(private rootDir: string) {
|
||||||
super();
|
super();
|
||||||
@@ -44,6 +46,74 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize all mutations to a given task's task.json by chaining promises
|
||||||
|
* per task ID. Concurrent callers for the same ID will queue behind each other.
|
||||||
|
*/
|
||||||
|
private withTaskLock<T>(id: string, fn: () => Promise<T>): Promise<T> {
|
||||||
|
const prev = this.taskLocks.get(id) ?? Promise.resolve();
|
||||||
|
let resolve: () => void;
|
||||||
|
const next = new Promise<void>((r) => { resolve = r; });
|
||||||
|
this.taskLocks.set(id, next);
|
||||||
|
|
||||||
|
return prev.then(async () => {
|
||||||
|
try {
|
||||||
|
return await fn();
|
||||||
|
} finally {
|
||||||
|
if (this.taskLocks.get(id) === next) {
|
||||||
|
this.taskLocks.delete(id);
|
||||||
|
}
|
||||||
|
resolve!();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely read and parse a task.json file. On `SyntaxError`, attempts to
|
||||||
|
* recover by truncating the content at the last valid `}` and re-parsing.
|
||||||
|
* Logs a warning to stderr when truncation-repair is used.
|
||||||
|
*/
|
||||||
|
private async safeReadTaskJson(dir: string): Promise<Task> {
|
||||||
|
const filePath = join(dir, "task.json");
|
||||||
|
const raw = await readFile(filePath, "utf-8");
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw) as Task;
|
||||||
|
} catch (err) {
|
||||||
|
if (!(err instanceof SyntaxError)) throw err;
|
||||||
|
|
||||||
|
// Attempt recovery: try truncating at each '}' from the end until valid
|
||||||
|
let pos = raw.length;
|
||||||
|
while ((pos = raw.lastIndexOf("}", pos - 1)) > 0) {
|
||||||
|
try {
|
||||||
|
const task = JSON.parse(raw.slice(0, pos + 1)) as Task;
|
||||||
|
console.warn(
|
||||||
|
`[hai] Warning: repaired corrupted task.json at ${filePath} (truncated ${raw.length - pos - 1} trailing bytes)`,
|
||||||
|
);
|
||||||
|
return task;
|
||||||
|
} catch {
|
||||||
|
// Try next position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Failed to parse task.json at ${filePath}: ${(err as Error).message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atomically write a task.json file by writing to a temp file first,
|
||||||
|
* then renaming it into place. The rename is atomic on POSIX filesystems,
|
||||||
|
* preventing partial writes from corrupting the file on crash/kill.
|
||||||
|
*/
|
||||||
|
private async atomicWriteTaskJson(dir: string, task: Task): Promise<void> {
|
||||||
|
const taskJsonPath = join(dir, "task.json");
|
||||||
|
const tmpPath = join(dir, "task.json.tmp");
|
||||||
|
this.suppressWatcher(taskJsonPath);
|
||||||
|
await writeFile(tmpPath, JSON.stringify(task, null, 2));
|
||||||
|
await rename(tmpPath, taskJsonPath);
|
||||||
|
}
|
||||||
|
|
||||||
async getSettings(): Promise<Settings> {
|
async getSettings(): Promise<Settings> {
|
||||||
const config = await this.readConfig();
|
const config = await this.readConfig();
|
||||||
return { ...DEFAULT_SETTINGS, ...config.settings };
|
return { ...DEFAULT_SETTINGS, ...config.settings };
|
||||||
@@ -101,9 +171,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
|||||||
|
|
||||||
const dir = this.taskDir(id);
|
const dir = this.taskDir(id);
|
||||||
await mkdir(dir, { recursive: true });
|
await mkdir(dir, { recursive: true });
|
||||||
const taskJsonPath = join(dir, "task.json");
|
await this.atomicWriteTaskJson(dir, task);
|
||||||
this.suppressWatcher(taskJsonPath);
|
|
||||||
await writeFile(taskJsonPath, JSON.stringify(task, null, 2));
|
|
||||||
|
|
||||||
// Update cache if watcher is active
|
// Update cache if watcher is active
|
||||||
if (this.watcher) this.taskCache.set(id, { ...task });
|
if (this.watcher) this.taskCache.set(id, { ...task });
|
||||||
@@ -120,8 +188,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
|||||||
|
|
||||||
async getTask(id: string): Promise<TaskDetail> {
|
async getTask(id: string): Promise<TaskDetail> {
|
||||||
const dir = this.taskDir(id);
|
const dir = this.taskDir(id);
|
||||||
const data = await readFile(join(dir, "task.json"), "utf-8");
|
const task = await this.safeReadTaskJson(dir);
|
||||||
const task = JSON.parse(data) as Task;
|
|
||||||
|
|
||||||
let prompt = "";
|
let prompt = "";
|
||||||
const promptPath = join(dir, "PROMPT.md");
|
const promptPath = join(dir, "PROMPT.md");
|
||||||
@@ -141,11 +208,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
|||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (entry.isDirectory() && entry.name.startsWith("HAI-")) {
|
if (entry.isDirectory() && entry.name.startsWith("HAI-")) {
|
||||||
try {
|
try {
|
||||||
const data = await readFile(
|
tasks.push(await this.safeReadTaskJson(join(this.tasksDir, entry.name)));
|
||||||
join(this.tasksDir, entry.name, "task.json"),
|
|
||||||
"utf-8",
|
|
||||||
);
|
|
||||||
tasks.push(JSON.parse(data));
|
|
||||||
} catch {
|
} catch {
|
||||||
// skip invalid task dirs
|
// skip invalid task dirs
|
||||||
}
|
}
|
||||||
@@ -156,70 +219,68 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async moveTask(id: string, toColumn: Column): Promise<Task> {
|
async moveTask(id: string, toColumn: Column): Promise<Task> {
|
||||||
const dir = this.taskDir(id);
|
return this.withTaskLock(id, async () => {
|
||||||
const data = await readFile(join(dir, "task.json"), "utf-8");
|
const dir = this.taskDir(id);
|
||||||
const task = JSON.parse(data) as Task;
|
const task = await this.safeReadTaskJson(dir);
|
||||||
|
|
||||||
const validTargets = VALID_TRANSITIONS[task.column];
|
const validTargets = VALID_TRANSITIONS[task.column];
|
||||||
if (!validTargets.includes(toColumn)) {
|
if (!validTargets.includes(toColumn)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Invalid transition: '${task.column}' → '${toColumn}'. ` +
|
`Invalid transition: '${task.column}' → '${toColumn}'. ` +
|
||||||
`Valid targets: ${validTargets.join(", ") || "none"}`,
|
`Valid targets: ${validTargets.join(", ") || "none"}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fromColumn = task.column;
|
const fromColumn = task.column;
|
||||||
task.column = toColumn;
|
task.column = toColumn;
|
||||||
task.updatedAt = new Date().toISOString();
|
task.updatedAt = new Date().toISOString();
|
||||||
|
|
||||||
// Clear transient fields when moving to done (matches moveToDone behavior)
|
// Clear transient fields when moving to done (matches moveToDone behavior)
|
||||||
if (toColumn === "done") {
|
if (toColumn === "done") {
|
||||||
task.status = undefined;
|
task.status = undefined;
|
||||||
task.worktree = undefined;
|
task.worktree = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const taskJsonPath = join(dir, "task.json");
|
await this.atomicWriteTaskJson(dir, task);
|
||||||
this.suppressWatcher(taskJsonPath);
|
|
||||||
await writeFile(taskJsonPath, JSON.stringify(task, null, 2));
|
|
||||||
|
|
||||||
// Update cache if watcher is active
|
// Update cache if watcher is active
|
||||||
if (this.watcher) this.taskCache.set(id, { ...task });
|
if (this.watcher) this.taskCache.set(id, { ...task });
|
||||||
|
|
||||||
this.emit("task:moved", { task, from: fromColumn, to: toColumn });
|
this.emit("task:moved", { task, from: fromColumn, to: toColumn });
|
||||||
return task;
|
return task;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateTask(
|
async updateTask(
|
||||||
id: string,
|
id: string,
|
||||||
updates: { title?: string; description?: string; prompt?: string; worktree?: string; status?: string | null },
|
updates: { title?: string; description?: string; prompt?: string; worktree?: string; status?: string | null },
|
||||||
): Promise<Task> {
|
): Promise<Task> {
|
||||||
const dir = this.taskDir(id);
|
return this.withTaskLock(id, async () => {
|
||||||
const data = await readFile(join(dir, "task.json"), "utf-8");
|
const dir = this.taskDir(id);
|
||||||
const task = JSON.parse(data) as Task;
|
const task = await this.safeReadTaskJson(dir);
|
||||||
|
|
||||||
if (updates.title !== undefined) task.title = updates.title;
|
if (updates.title !== undefined) task.title = updates.title;
|
||||||
if (updates.description !== undefined) task.description = updates.description;
|
if (updates.description !== undefined) task.description = updates.description;
|
||||||
if (updates.worktree !== undefined) task.worktree = updates.worktree;
|
if (updates.worktree !== undefined) task.worktree = updates.worktree;
|
||||||
if (updates.status === null) {
|
if (updates.status === null) {
|
||||||
task.status = undefined;
|
task.status = undefined;
|
||||||
} else if (updates.status !== undefined) {
|
} else if (updates.status !== undefined) {
|
||||||
task.status = updates.status;
|
task.status = updates.status;
|
||||||
}
|
}
|
||||||
task.updatedAt = new Date().toISOString();
|
task.updatedAt = new Date().toISOString();
|
||||||
|
|
||||||
const taskJsonPath = join(dir, "task.json");
|
await this.atomicWriteTaskJson(dir, task);
|
||||||
this.suppressWatcher(taskJsonPath);
|
|
||||||
await writeFile(taskJsonPath, JSON.stringify(task, null, 2));
|
|
||||||
|
|
||||||
// Update cache if watcher is active
|
// Update cache if watcher is active
|
||||||
if (this.watcher) this.taskCache.set(id, { ...task });
|
if (this.watcher) this.taskCache.set(id, { ...task });
|
||||||
|
|
||||||
if (updates.prompt !== undefined) {
|
if (updates.prompt !== undefined) {
|
||||||
await writeFile(join(dir, "PROMPT.md"), updates.prompt);
|
await writeFile(join(dir, "PROMPT.md"), updates.prompt);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.emit("task:updated", task);
|
this.emit("task:updated", task);
|
||||||
return task;
|
return task;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -230,73 +291,71 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
|||||||
stepIndex: number,
|
stepIndex: number,
|
||||||
status: import("./types.js").StepStatus,
|
status: import("./types.js").StepStatus,
|
||||||
): Promise<Task> {
|
): Promise<Task> {
|
||||||
const dir = this.taskDir(id);
|
return this.withTaskLock(id, async () => {
|
||||||
const data = await readFile(join(dir, "task.json"), "utf-8");
|
const dir = this.taskDir(id);
|
||||||
const task = JSON.parse(data) as Task;
|
const task = await this.safeReadTaskJson(dir);
|
||||||
|
|
||||||
// Auto-initialize steps from PROMPT.md if empty
|
// Auto-initialize steps from PROMPT.md if empty
|
||||||
if (task.steps.length === 0) {
|
if (task.steps.length === 0) {
|
||||||
task.steps = await this.parseStepsFromPrompt(id);
|
task.steps = await this.parseStepsFromPrompt(id);
|
||||||
}
|
|
||||||
|
|
||||||
if (stepIndex < 0 || stepIndex >= task.steps.length) {
|
|
||||||
throw new Error(
|
|
||||||
`Step ${stepIndex} out of range (task has ${task.steps.length} steps)`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
task.steps[stepIndex].status = status;
|
|
||||||
task.updatedAt = new Date().toISOString();
|
|
||||||
|
|
||||||
// Advance currentStep to first non-done step
|
|
||||||
if (status === "done") {
|
|
||||||
while (
|
|
||||||
task.currentStep < task.steps.length &&
|
|
||||||
task.steps[task.currentStep].status === "done"
|
|
||||||
) {
|
|
||||||
task.currentStep++;
|
|
||||||
}
|
}
|
||||||
} else if (status === "in-progress") {
|
|
||||||
task.currentStep = stepIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log it
|
if (stepIndex < 0 || stepIndex >= task.steps.length) {
|
||||||
task.log.push({
|
throw new Error(
|
||||||
timestamp: task.updatedAt,
|
`Step ${stepIndex} out of range (task has ${task.steps.length} steps)`,
|
||||||
action: `Step ${stepIndex} (${task.steps[stepIndex].name}) → ${status}`,
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
task.steps[stepIndex].status = status;
|
||||||
|
task.updatedAt = new Date().toISOString();
|
||||||
|
|
||||||
|
// Advance currentStep to first non-done step
|
||||||
|
if (status === "done") {
|
||||||
|
while (
|
||||||
|
task.currentStep < task.steps.length &&
|
||||||
|
task.steps[task.currentStep].status === "done"
|
||||||
|
) {
|
||||||
|
task.currentStep++;
|
||||||
|
}
|
||||||
|
} else if (status === "in-progress") {
|
||||||
|
task.currentStep = stepIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log it
|
||||||
|
task.log.push({
|
||||||
|
timestamp: task.updatedAt,
|
||||||
|
action: `Step ${stepIndex} (${task.steps[stepIndex].name}) → ${status}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.atomicWriteTaskJson(dir, task);
|
||||||
|
if (this.watcher) this.taskCache.set(id, { ...task });
|
||||||
|
|
||||||
|
this.emit("task:updated", task);
|
||||||
|
return task;
|
||||||
});
|
});
|
||||||
|
|
||||||
const taskJsonPath = join(dir, "task.json");
|
|
||||||
this.suppressWatcher(taskJsonPath);
|
|
||||||
await writeFile(taskJsonPath, JSON.stringify(task, null, 2));
|
|
||||||
if (this.watcher) this.taskCache.set(id, { ...task });
|
|
||||||
|
|
||||||
this.emit("task:updated", task);
|
|
||||||
return task;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a log entry to a task.
|
* Add a log entry to a task.
|
||||||
*/
|
*/
|
||||||
async logEntry(id: string, action: string, outcome?: string): Promise<Task> {
|
async logEntry(id: string, action: string, outcome?: string): Promise<Task> {
|
||||||
const dir = this.taskDir(id);
|
return this.withTaskLock(id, async () => {
|
||||||
const data = await readFile(join(dir, "task.json"), "utf-8");
|
const dir = this.taskDir(id);
|
||||||
const task = JSON.parse(data) as Task;
|
const task = await this.safeReadTaskJson(dir);
|
||||||
|
|
||||||
task.log.push({
|
task.log.push({
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
action,
|
action,
|
||||||
outcome,
|
outcome,
|
||||||
|
});
|
||||||
|
task.updatedAt = new Date().toISOString();
|
||||||
|
|
||||||
|
await this.atomicWriteTaskJson(dir, task);
|
||||||
|
if (this.watcher) this.taskCache.set(id, { ...task });
|
||||||
|
|
||||||
|
this.emit("task:updated", task);
|
||||||
|
return task;
|
||||||
});
|
});
|
||||||
task.updatedAt = new Date().toISOString();
|
|
||||||
|
|
||||||
const taskJsonPath = join(dir, "task.json");
|
|
||||||
this.suppressWatcher(taskJsonPath);
|
|
||||||
await writeFile(taskJsonPath, JSON.stringify(task, null, 2));
|
|
||||||
if (this.watcher) this.taskCache.set(id, { ...task });
|
|
||||||
|
|
||||||
this.emit("task:updated", task);
|
|
||||||
return task;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -347,21 +406,22 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteTask(id: string): Promise<Task> {
|
async deleteTask(id: string): Promise<Task> {
|
||||||
const dir = this.taskDir(id);
|
return this.withTaskLock(id, async () => {
|
||||||
const data = await readFile(join(dir, "task.json"), "utf-8");
|
const dir = this.taskDir(id);
|
||||||
const task = JSON.parse(data) as Task;
|
const task = await this.safeReadTaskJson(dir);
|
||||||
|
|
||||||
const taskJsonPath = join(dir, "task.json");
|
const taskJsonPath = join(dir, "task.json");
|
||||||
this.suppressWatcher(taskJsonPath);
|
this.suppressWatcher(taskJsonPath);
|
||||||
|
|
||||||
// Remove from cache if watcher is active
|
// Remove from cache if watcher is active
|
||||||
if (this.watcher) this.taskCache.delete(id);
|
if (this.watcher) this.taskCache.delete(id);
|
||||||
|
|
||||||
const { rm } = await import("node:fs/promises");
|
const { rm } = await import("node:fs/promises");
|
||||||
await rm(dir, { recursive: true });
|
await rm(dir, { recursive: true });
|
||||||
|
|
||||||
this.emit("task:deleted", task);
|
this.emit("task:deleted", task);
|
||||||
return task;
|
return task;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -369,102 +429,103 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
|||||||
* clean up the worktree, and move the task to done.
|
* clean up the worktree, and move the task to done.
|
||||||
*/
|
*/
|
||||||
async mergeTask(id: string): Promise<MergeResult> {
|
async mergeTask(id: string): Promise<MergeResult> {
|
||||||
const dir = this.taskDir(id);
|
return this.withTaskLock(id, async () => {
|
||||||
const data = await readFile(join(dir, "task.json"), "utf-8");
|
const dir = this.taskDir(id);
|
||||||
const task = JSON.parse(data) as Task;
|
const task = await this.safeReadTaskJson(dir);
|
||||||
|
|
||||||
if (task.column !== "in-review") {
|
if (task.column !== "in-review") {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Cannot merge ${id}: task is in '${task.column}', must be in 'in-review'`,
|
`Cannot merge ${id}: task is in '${task.column}', must be in 'in-review'`,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
const branch = `hai/${id.toLowerCase()}`;
|
|
||||||
const worktreePath = task.worktree || join(this.rootDir, ".worktrees", id);
|
|
||||||
const result: MergeResult = {
|
|
||||||
task,
|
|
||||||
branch,
|
|
||||||
merged: false,
|
|
||||||
worktreeRemoved: false,
|
|
||||||
branchDeleted: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 1. Check the branch exists
|
|
||||||
try {
|
|
||||||
execSync(`git rev-parse --verify "${branch}"`, {
|
|
||||||
cwd: this.rootDir,
|
|
||||||
stdio: "pipe",
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
// No branch — might have been manually merged. Just move to done.
|
|
||||||
result.error = `Branch '${branch}' not found — moving to done without merge`;
|
|
||||||
await this.moveToDone(task, dir);
|
|
||||||
result.task = { ...task, column: "done" };
|
|
||||||
this.emit("task:merged", result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Merge the branch
|
|
||||||
try {
|
|
||||||
execSync(`git merge "${branch}" --no-edit`, {
|
|
||||||
cwd: this.rootDir,
|
|
||||||
stdio: "pipe",
|
|
||||||
});
|
|
||||||
result.merged = true;
|
|
||||||
} catch (err: any) {
|
|
||||||
// Merge conflict — abort and report
|
|
||||||
try {
|
|
||||||
execSync("git merge --abort", { cwd: this.rootDir, stdio: "pipe" });
|
|
||||||
} catch {
|
|
||||||
// already clean
|
|
||||||
}
|
}
|
||||||
throw new Error(
|
|
||||||
`Merge conflict merging '${branch}'. Resolve manually:\n` +
|
|
||||||
` cd ${this.rootDir}\n` +
|
|
||||||
` git merge ${branch}\n` +
|
|
||||||
` # resolve conflicts, then: hai task move ${id} done`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Remove worktree
|
const branch = `hai/${id.toLowerCase()}`;
|
||||||
if (existsSync(worktreePath)) {
|
const worktreePath = task.worktree || join(this.rootDir, ".worktrees", id);
|
||||||
|
const result: MergeResult = {
|
||||||
|
task,
|
||||||
|
branch,
|
||||||
|
merged: false,
|
||||||
|
worktreeRemoved: false,
|
||||||
|
branchDeleted: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. Check the branch exists
|
||||||
try {
|
try {
|
||||||
execSync(`git worktree remove "${worktreePath}" --force`, {
|
execSync(`git rev-parse --verify "${branch}"`, {
|
||||||
cwd: this.rootDir,
|
cwd: this.rootDir,
|
||||||
stdio: "pipe",
|
stdio: "pipe",
|
||||||
});
|
});
|
||||||
result.worktreeRemoved = true;
|
|
||||||
} catch {
|
} catch {
|
||||||
// Non-fatal — worktree may already be gone
|
// No branch — might have been manually merged. Just move to done.
|
||||||
|
result.error = `Branch '${branch}' not found — moving to done without merge`;
|
||||||
|
await this.moveToDone(task, dir);
|
||||||
|
result.task = { ...task, column: "done" };
|
||||||
|
this.emit("task:merged", result);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Delete the branch
|
// 2. Merge the branch
|
||||||
try {
|
|
||||||
execSync(`git branch -d "${branch}"`, {
|
|
||||||
cwd: this.rootDir,
|
|
||||||
stdio: "pipe",
|
|
||||||
});
|
|
||||||
result.branchDeleted = true;
|
|
||||||
} catch {
|
|
||||||
// Branch might not be fully merged in some edge cases; try force
|
|
||||||
try {
|
try {
|
||||||
execSync(`git branch -D "${branch}"`, {
|
execSync(`git merge "${branch}" --no-edit`, {
|
||||||
|
cwd: this.rootDir,
|
||||||
|
stdio: "pipe",
|
||||||
|
});
|
||||||
|
result.merged = true;
|
||||||
|
} catch (err: any) {
|
||||||
|
// Merge conflict — abort and report
|
||||||
|
try {
|
||||||
|
execSync("git merge --abort", { cwd: this.rootDir, stdio: "pipe" });
|
||||||
|
} catch {
|
||||||
|
// already clean
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`Merge conflict merging '${branch}'. Resolve manually:\n` +
|
||||||
|
` cd ${this.rootDir}\n` +
|
||||||
|
` git merge ${branch}\n` +
|
||||||
|
` # resolve conflicts, then: hai task move ${id} done`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Remove worktree
|
||||||
|
if (existsSync(worktreePath)) {
|
||||||
|
try {
|
||||||
|
execSync(`git worktree remove "${worktreePath}" --force`, {
|
||||||
|
cwd: this.rootDir,
|
||||||
|
stdio: "pipe",
|
||||||
|
});
|
||||||
|
result.worktreeRemoved = true;
|
||||||
|
} catch {
|
||||||
|
// Non-fatal — worktree may already be gone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Delete the branch
|
||||||
|
try {
|
||||||
|
execSync(`git branch -d "${branch}"`, {
|
||||||
cwd: this.rootDir,
|
cwd: this.rootDir,
|
||||||
stdio: "pipe",
|
stdio: "pipe",
|
||||||
});
|
});
|
||||||
result.branchDeleted = true;
|
result.branchDeleted = true;
|
||||||
} catch {
|
} catch {
|
||||||
// Non-fatal
|
// Branch might not be fully merged in some edge cases; try force
|
||||||
|
try {
|
||||||
|
execSync(`git branch -D "${branch}"`, {
|
||||||
|
cwd: this.rootDir,
|
||||||
|
stdio: "pipe",
|
||||||
|
});
|
||||||
|
result.branchDeleted = true;
|
||||||
|
} catch {
|
||||||
|
// Non-fatal
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Move task to done
|
// 5. Move task to done
|
||||||
await this.moveToDone(task, dir);
|
await this.moveToDone(task, dir);
|
||||||
result.task = { ...task, column: "done" };
|
result.task = { ...task, column: "done" };
|
||||||
|
|
||||||
this.emit("task:merged", result);
|
this.emit("task:merged", result);
|
||||||
return result;
|
return result;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async moveToDone(task: Task, dir: string): Promise<void> {
|
private async moveToDone(task: Task, dir: string): Promise<void> {
|
||||||
@@ -473,9 +534,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
|||||||
task.status = undefined;
|
task.status = undefined;
|
||||||
task.updatedAt = new Date().toISOString();
|
task.updatedAt = new Date().toISOString();
|
||||||
|
|
||||||
const taskJsonPath = join(dir, "task.json");
|
await this.atomicWriteTaskJson(dir, task);
|
||||||
this.suppressWatcher(taskJsonPath);
|
|
||||||
await writeFile(taskJsonPath, JSON.stringify(task, null, 2));
|
|
||||||
|
|
||||||
// Update cache if watcher is active
|
// Update cache if watcher is active
|
||||||
if (this.watcher) this.taskCache.set(task.id, { ...task });
|
if (this.watcher) this.taskCache.set(task.id, { ...task });
|
||||||
@@ -594,8 +653,8 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
|||||||
|
|
||||||
let task: Task;
|
let task: Task;
|
||||||
try {
|
try {
|
||||||
const data = await readFile(filePath, "utf-8");
|
const taskDir = join(this.tasksDir, taskId);
|
||||||
task = JSON.parse(data) as Task;
|
task = await this.safeReadTaskJson(taskDir);
|
||||||
} catch {
|
} catch {
|
||||||
return; // File not readable or invalid JSON
|
return; // File not readable or invalid JSON
|
||||||
}
|
}
|
||||||
|
|||||||
7
packages/core/vitest.config.ts
Normal file
7
packages/core/vitest.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
include: ["src/**/*.test.ts"],
|
||||||
|
},
|
||||||
|
});
|
||||||
252
pnpm-lock.yaml
generated
252
pnpm-lock.yaml
generated
@@ -42,6 +42,9 @@ importers:
|
|||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.7.0
|
specifier: ^5.7.0
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
|
vitest:
|
||||||
|
specifier: ^3.1.0
|
||||||
|
version: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jsdom@29.0.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||||
|
|
||||||
packages/dashboard:
|
packages/dashboard:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1354,9 +1357,23 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||||
|
|
||||||
|
'@vitest/expect@3.2.4':
|
||||||
|
resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==}
|
||||||
|
|
||||||
'@vitest/expect@4.1.1':
|
'@vitest/expect@4.1.1':
|
||||||
resolution: {integrity: sha512-xAV0fqBTk44Rn6SjJReEQkHP3RrqbJo6JQ4zZ7/uVOiJZRarBtblzrOfFIZeYUrukp2YD6snZG6IBqhOoHTm+A==}
|
resolution: {integrity: sha512-xAV0fqBTk44Rn6SjJReEQkHP3RrqbJo6JQ4zZ7/uVOiJZRarBtblzrOfFIZeYUrukp2YD6snZG6IBqhOoHTm+A==}
|
||||||
|
|
||||||
|
'@vitest/mocker@3.2.4':
|
||||||
|
resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==}
|
||||||
|
peerDependencies:
|
||||||
|
msw: ^2.4.9
|
||||||
|
vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
msw:
|
||||||
|
optional: true
|
||||||
|
vite:
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@vitest/mocker@4.1.1':
|
'@vitest/mocker@4.1.1':
|
||||||
resolution: {integrity: sha512-h3BOylsfsCLPeceuCPAAJ+BvNwSENgJa4hXoXu4im0bs9Lyp4URc4JYK4pWLZ4pG/UQn7AT92K6IByi6rE6g3A==}
|
resolution: {integrity: sha512-h3BOylsfsCLPeceuCPAAJ+BvNwSENgJa4hXoXu4im0bs9Lyp4URc4JYK4pWLZ4pG/UQn7AT92K6IByi6rE6g3A==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1368,18 +1385,33 @@ packages:
|
|||||||
vite:
|
vite:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@vitest/pretty-format@3.2.4':
|
||||||
|
resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==}
|
||||||
|
|
||||||
'@vitest/pretty-format@4.1.1':
|
'@vitest/pretty-format@4.1.1':
|
||||||
resolution: {integrity: sha512-GM+TEQN5WhOygr1lp7skeVjdLPqqWMHsfzXrcHAqZJi/lIVh63H0kaRCY8MDhNWikx19zBUK8ceaLB7X5AH9NQ==}
|
resolution: {integrity: sha512-GM+TEQN5WhOygr1lp7skeVjdLPqqWMHsfzXrcHAqZJi/lIVh63H0kaRCY8MDhNWikx19zBUK8ceaLB7X5AH9NQ==}
|
||||||
|
|
||||||
|
'@vitest/runner@3.2.4':
|
||||||
|
resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==}
|
||||||
|
|
||||||
'@vitest/runner@4.1.1':
|
'@vitest/runner@4.1.1':
|
||||||
resolution: {integrity: sha512-f7+FPy75vN91QGWsITueq0gedwUZy1fLtHOCMeQpjs8jTekAHeKP80zfDEnhrleviLHzVSDXIWuCIOFn3D3f8A==}
|
resolution: {integrity: sha512-f7+FPy75vN91QGWsITueq0gedwUZy1fLtHOCMeQpjs8jTekAHeKP80zfDEnhrleviLHzVSDXIWuCIOFn3D3f8A==}
|
||||||
|
|
||||||
|
'@vitest/snapshot@3.2.4':
|
||||||
|
resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==}
|
||||||
|
|
||||||
'@vitest/snapshot@4.1.1':
|
'@vitest/snapshot@4.1.1':
|
||||||
resolution: {integrity: sha512-kMVSgcegWV2FibXEx9p9WIKgje58lcTbXgnJixfcg15iK8nzCXhmalL0ZLtTWLW9PH1+1NEDShiFFedB3tEgWg==}
|
resolution: {integrity: sha512-kMVSgcegWV2FibXEx9p9WIKgje58lcTbXgnJixfcg15iK8nzCXhmalL0ZLtTWLW9PH1+1NEDShiFFedB3tEgWg==}
|
||||||
|
|
||||||
|
'@vitest/spy@3.2.4':
|
||||||
|
resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==}
|
||||||
|
|
||||||
'@vitest/spy@4.1.1':
|
'@vitest/spy@4.1.1':
|
||||||
resolution: {integrity: sha512-6Ti/KT5OVaiupdIZEuZN7l3CZcR0cxnxt70Z0//3CtwgObwA6jZhmVBA3yrXSVN3gmwjgd7oDNLlsXz526gpRA==}
|
resolution: {integrity: sha512-6Ti/KT5OVaiupdIZEuZN7l3CZcR0cxnxt70Z0//3CtwgObwA6jZhmVBA3yrXSVN3gmwjgd7oDNLlsXz526gpRA==}
|
||||||
|
|
||||||
|
'@vitest/utils@3.2.4':
|
||||||
|
resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}
|
||||||
|
|
||||||
'@vitest/utils@4.1.1':
|
'@vitest/utils@4.1.1':
|
||||||
resolution: {integrity: sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ==}
|
resolution: {integrity: sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ==}
|
||||||
|
|
||||||
@@ -1487,6 +1519,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
cac@6.7.14:
|
||||||
|
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
call-bind-apply-helpers@1.0.2:
|
call-bind-apply-helpers@1.0.2:
|
||||||
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -1501,6 +1537,10 @@ packages:
|
|||||||
ccount@2.0.1:
|
ccount@2.0.1:
|
||||||
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
||||||
|
|
||||||
|
chai@5.3.3:
|
||||||
|
resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
chai@6.2.2:
|
chai@6.2.2:
|
||||||
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
|
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -1525,6 +1565,10 @@ packages:
|
|||||||
character-reference-invalid@2.0.1:
|
character-reference-invalid@2.0.1:
|
||||||
resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
|
resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
|
||||||
|
|
||||||
|
check-error@2.1.3:
|
||||||
|
resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
|
||||||
|
engines: {node: '>= 16'}
|
||||||
|
|
||||||
cli-highlight@2.1.11:
|
cli-highlight@2.1.11:
|
||||||
resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==}
|
resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==}
|
||||||
engines: {node: '>=8.0.0', npm: '>=5.0.0'}
|
engines: {node: '>=8.0.0', npm: '>=5.0.0'}
|
||||||
@@ -1599,6 +1643,10 @@ packages:
|
|||||||
decode-named-character-reference@1.3.0:
|
decode-named-character-reference@1.3.0:
|
||||||
resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==}
|
resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==}
|
||||||
|
|
||||||
|
deep-eql@5.0.2:
|
||||||
|
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
degenerator@5.0.1:
|
degenerator@5.0.1:
|
||||||
resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==}
|
resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==}
|
||||||
engines: {node: '>= 14'}
|
engines: {node: '>= 14'}
|
||||||
@@ -1659,6 +1707,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
|
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
es-module-lexer@1.7.0:
|
||||||
|
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
|
||||||
|
|
||||||
es-module-lexer@2.0.0:
|
es-module-lexer@2.0.0:
|
||||||
resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==}
|
resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==}
|
||||||
|
|
||||||
@@ -1948,6 +1999,9 @@ packages:
|
|||||||
js-tokens@4.0.0:
|
js-tokens@4.0.0:
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
|
|
||||||
|
js-tokens@9.0.1:
|
||||||
|
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
|
||||||
|
|
||||||
jsdom@29.0.1:
|
jsdom@29.0.1:
|
||||||
resolution: {integrity: sha512-z6JOK5gRO7aMybVq/y/MlIpKh8JIi68FBKMUtKkK2KH/wMSRlCxQ682d08LB9fYXplyY/UXG8P4XXTScmdjApg==}
|
resolution: {integrity: sha512-z6JOK5gRO7aMybVq/y/MlIpKh8JIi68FBKMUtKkK2KH/wMSRlCxQ682d08LB9fYXplyY/UXG8P4XXTScmdjApg==}
|
||||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0}
|
engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0}
|
||||||
@@ -1992,6 +2046,9 @@ packages:
|
|||||||
longest-streak@3.1.0:
|
longest-streak@3.1.0:
|
||||||
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
||||||
|
|
||||||
|
loupe@3.2.1:
|
||||||
|
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
|
||||||
|
|
||||||
lru-cache@11.2.7:
|
lru-cache@11.2.7:
|
||||||
resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==}
|
resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
@@ -2296,6 +2353,10 @@ packages:
|
|||||||
pathe@2.0.3:
|
pathe@2.0.3:
|
||||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||||
|
|
||||||
|
pathval@2.0.1:
|
||||||
|
resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
|
||||||
|
engines: {node: '>= 14.16'}
|
||||||
|
|
||||||
pend@1.2.0:
|
pend@1.2.0:
|
||||||
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
|
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
|
||||||
|
|
||||||
@@ -2525,6 +2586,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
|
resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
strip-literal@3.1.0:
|
||||||
|
resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==}
|
||||||
|
|
||||||
strnum@2.2.2:
|
strnum@2.2.2:
|
||||||
resolution: {integrity: sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==}
|
resolution: {integrity: sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==}
|
||||||
|
|
||||||
@@ -2555,6 +2619,9 @@ packages:
|
|||||||
tinybench@2.9.0:
|
tinybench@2.9.0:
|
||||||
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||||
|
|
||||||
|
tinyexec@0.3.2:
|
||||||
|
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
|
||||||
|
|
||||||
tinyexec@1.0.4:
|
tinyexec@1.0.4:
|
||||||
resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==}
|
resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -2563,10 +2630,22 @@ packages:
|
|||||||
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
tinypool@1.1.1:
|
||||||
|
resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==}
|
||||||
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
|
|
||||||
|
tinyrainbow@2.0.0:
|
||||||
|
resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
tinyrainbow@3.1.0:
|
tinyrainbow@3.1.0:
|
||||||
resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==}
|
resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
|
tinyspy@4.0.4:
|
||||||
|
resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
tldts-core@7.0.27:
|
tldts-core@7.0.27:
|
||||||
resolution: {integrity: sha512-YQ7uPjgWUibIK6DW5lrKujGwUKhLevU4hcGbP5O6TcIUb+oTjJYJVWPS4nZsIHrEEEG6myk/oqAJUEQmpZrHsg==}
|
resolution: {integrity: sha512-YQ7uPjgWUibIK6DW5lrKujGwUKhLevU4hcGbP5O6TcIUb+oTjJYJVWPS4nZsIHrEEEG6myk/oqAJUEQmpZrHsg==}
|
||||||
|
|
||||||
@@ -2665,6 +2744,11 @@ packages:
|
|||||||
vfile@6.0.3:
|
vfile@6.0.3:
|
||||||
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
|
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
|
||||||
|
|
||||||
|
vite-node@3.2.4:
|
||||||
|
resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==}
|
||||||
|
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
vite@6.4.1:
|
vite@6.4.1:
|
||||||
resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==}
|
resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==}
|
||||||
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||||
@@ -2705,6 +2789,34 @@ packages:
|
|||||||
yaml:
|
yaml:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
vitest@3.2.4:
|
||||||
|
resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==}
|
||||||
|
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
'@edge-runtime/vm': '*'
|
||||||
|
'@types/debug': ^4.1.12
|
||||||
|
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
|
||||||
|
'@vitest/browser': 3.2.4
|
||||||
|
'@vitest/ui': 3.2.4
|
||||||
|
happy-dom: '*'
|
||||||
|
jsdom: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@edge-runtime/vm':
|
||||||
|
optional: true
|
||||||
|
'@types/debug':
|
||||||
|
optional: true
|
||||||
|
'@types/node':
|
||||||
|
optional: true
|
||||||
|
'@vitest/browser':
|
||||||
|
optional: true
|
||||||
|
'@vitest/ui':
|
||||||
|
optional: true
|
||||||
|
happy-dom:
|
||||||
|
optional: true
|
||||||
|
jsdom:
|
||||||
|
optional: true
|
||||||
|
|
||||||
vitest@4.1.1:
|
vitest@4.1.1:
|
||||||
resolution: {integrity: sha512-yF+o4POL41rpAzj5KVILUxm1GCjKnELvaqmU9TLLUbMfDzuN0UpUR9uaDs+mCtjPe+uYPksXDRLQGGPvj1cTmA==}
|
resolution: {integrity: sha512-yF+o4POL41rpAzj5KVILUxm1GCjKnELvaqmU9TLLUbMfDzuN0UpUR9uaDs+mCtjPe+uYPksXDRLQGGPvj1cTmA==}
|
||||||
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
|
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||||
@@ -4291,6 +4403,14 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
'@vitest/expect@3.2.4':
|
||||||
|
dependencies:
|
||||||
|
'@types/chai': 5.2.3
|
||||||
|
'@vitest/spy': 3.2.4
|
||||||
|
'@vitest/utils': 3.2.4
|
||||||
|
chai: 5.3.3
|
||||||
|
tinyrainbow: 2.0.0
|
||||||
|
|
||||||
'@vitest/expect@4.1.1':
|
'@vitest/expect@4.1.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@standard-schema/spec': 1.1.0
|
'@standard-schema/spec': 1.1.0
|
||||||
@@ -4300,6 +4420,14 @@ snapshots:
|
|||||||
chai: 6.2.2
|
chai: 6.2.2
|
||||||
tinyrainbow: 3.1.0
|
tinyrainbow: 3.1.0
|
||||||
|
|
||||||
|
'@vitest/mocker@3.2.4(vite@6.4.1(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3))':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/spy': 3.2.4
|
||||||
|
estree-walker: 3.0.3
|
||||||
|
magic-string: 0.30.21
|
||||||
|
optionalDependencies:
|
||||||
|
vite: 6.4.1(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3)
|
||||||
|
|
||||||
'@vitest/mocker@4.1.1(vite@6.4.1(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3))':
|
'@vitest/mocker@4.1.1(vite@6.4.1(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/spy': 4.1.1
|
'@vitest/spy': 4.1.1
|
||||||
@@ -4308,15 +4436,31 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vite: 6.4.1(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3)
|
vite: 6.4.1(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3)
|
||||||
|
|
||||||
|
'@vitest/pretty-format@3.2.4':
|
||||||
|
dependencies:
|
||||||
|
tinyrainbow: 2.0.0
|
||||||
|
|
||||||
'@vitest/pretty-format@4.1.1':
|
'@vitest/pretty-format@4.1.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
tinyrainbow: 3.1.0
|
tinyrainbow: 3.1.0
|
||||||
|
|
||||||
|
'@vitest/runner@3.2.4':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/utils': 3.2.4
|
||||||
|
pathe: 2.0.3
|
||||||
|
strip-literal: 3.1.0
|
||||||
|
|
||||||
'@vitest/runner@4.1.1':
|
'@vitest/runner@4.1.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/utils': 4.1.1
|
'@vitest/utils': 4.1.1
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
|
|
||||||
|
'@vitest/snapshot@3.2.4':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/pretty-format': 3.2.4
|
||||||
|
magic-string: 0.30.21
|
||||||
|
pathe: 2.0.3
|
||||||
|
|
||||||
'@vitest/snapshot@4.1.1':
|
'@vitest/snapshot@4.1.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/pretty-format': 4.1.1
|
'@vitest/pretty-format': 4.1.1
|
||||||
@@ -4324,8 +4468,18 @@ snapshots:
|
|||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
|
|
||||||
|
'@vitest/spy@3.2.4':
|
||||||
|
dependencies:
|
||||||
|
tinyspy: 4.0.4
|
||||||
|
|
||||||
'@vitest/spy@4.1.1': {}
|
'@vitest/spy@4.1.1': {}
|
||||||
|
|
||||||
|
'@vitest/utils@3.2.4':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/pretty-format': 3.2.4
|
||||||
|
loupe: 3.2.1
|
||||||
|
tinyrainbow: 2.0.0
|
||||||
|
|
||||||
'@vitest/utils@4.1.1':
|
'@vitest/utils@4.1.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/pretty-format': 4.1.1
|
'@vitest/pretty-format': 4.1.1
|
||||||
@@ -4424,6 +4578,8 @@ snapshots:
|
|||||||
|
|
||||||
bytes@3.1.2: {}
|
bytes@3.1.2: {}
|
||||||
|
|
||||||
|
cac@6.7.14: {}
|
||||||
|
|
||||||
call-bind-apply-helpers@1.0.2:
|
call-bind-apply-helpers@1.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
@@ -4438,6 +4594,14 @@ snapshots:
|
|||||||
|
|
||||||
ccount@2.0.1: {}
|
ccount@2.0.1: {}
|
||||||
|
|
||||||
|
chai@5.3.3:
|
||||||
|
dependencies:
|
||||||
|
assertion-error: 2.0.1
|
||||||
|
check-error: 2.1.3
|
||||||
|
deep-eql: 5.0.2
|
||||||
|
loupe: 3.2.1
|
||||||
|
pathval: 2.0.1
|
||||||
|
|
||||||
chai@6.2.2: {}
|
chai@6.2.2: {}
|
||||||
|
|
||||||
chalk@4.1.2:
|
chalk@4.1.2:
|
||||||
@@ -4455,6 +4619,8 @@ snapshots:
|
|||||||
|
|
||||||
character-reference-invalid@2.0.1: {}
|
character-reference-invalid@2.0.1: {}
|
||||||
|
|
||||||
|
check-error@2.1.3: {}
|
||||||
|
|
||||||
cli-highlight@2.1.11:
|
cli-highlight@2.1.11:
|
||||||
dependencies:
|
dependencies:
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
@@ -4518,6 +4684,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
character-entities: 2.0.2
|
character-entities: 2.0.2
|
||||||
|
|
||||||
|
deep-eql@5.0.2: {}
|
||||||
|
|
||||||
degenerator@5.0.1:
|
degenerator@5.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
ast-types: 0.13.4
|
ast-types: 0.13.4
|
||||||
@@ -4566,6 +4734,8 @@ snapshots:
|
|||||||
|
|
||||||
es-errors@1.3.0: {}
|
es-errors@1.3.0: {}
|
||||||
|
|
||||||
|
es-module-lexer@1.7.0: {}
|
||||||
|
|
||||||
es-module-lexer@2.0.0: {}
|
es-module-lexer@2.0.0: {}
|
||||||
|
|
||||||
es-object-atoms@1.1.1:
|
es-object-atoms@1.1.1:
|
||||||
@@ -4951,6 +5121,8 @@ snapshots:
|
|||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
|
|
||||||
|
js-tokens@9.0.1: {}
|
||||||
|
|
||||||
jsdom@29.0.1:
|
jsdom@29.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@asamuzakjp/css-color': 5.0.1
|
'@asamuzakjp/css-color': 5.0.1
|
||||||
@@ -5010,6 +5182,8 @@ snapshots:
|
|||||||
|
|
||||||
longest-streak@3.1.0: {}
|
longest-streak@3.1.0: {}
|
||||||
|
|
||||||
|
loupe@3.2.1: {}
|
||||||
|
|
||||||
lru-cache@11.2.7: {}
|
lru-cache@11.2.7: {}
|
||||||
|
|
||||||
lru-cache@5.1.1:
|
lru-cache@5.1.1:
|
||||||
@@ -5501,6 +5675,8 @@ snapshots:
|
|||||||
|
|
||||||
pathe@2.0.3: {}
|
pathe@2.0.3: {}
|
||||||
|
|
||||||
|
pathval@2.0.1: {}
|
||||||
|
|
||||||
pend@1.2.0: {}
|
pend@1.2.0: {}
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
@@ -5825,6 +6001,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
min-indent: 1.0.1
|
min-indent: 1.0.1
|
||||||
|
|
||||||
|
strip-literal@3.1.0:
|
||||||
|
dependencies:
|
||||||
|
js-tokens: 9.0.1
|
||||||
|
|
||||||
strnum@2.2.2: {}
|
strnum@2.2.2: {}
|
||||||
|
|
||||||
strtok3@10.3.5:
|
strtok3@10.3.5:
|
||||||
@@ -5855,6 +6035,8 @@ snapshots:
|
|||||||
|
|
||||||
tinybench@2.9.0: {}
|
tinybench@2.9.0: {}
|
||||||
|
|
||||||
|
tinyexec@0.3.2: {}
|
||||||
|
|
||||||
tinyexec@1.0.4: {}
|
tinyexec@1.0.4: {}
|
||||||
|
|
||||||
tinyglobby@0.2.15:
|
tinyglobby@0.2.15:
|
||||||
@@ -5862,8 +6044,14 @@ snapshots:
|
|||||||
fdir: 6.5.0(picomatch@4.0.4)
|
fdir: 6.5.0(picomatch@4.0.4)
|
||||||
picomatch: 4.0.4
|
picomatch: 4.0.4
|
||||||
|
|
||||||
|
tinypool@1.1.1: {}
|
||||||
|
|
||||||
|
tinyrainbow@2.0.0: {}
|
||||||
|
|
||||||
tinyrainbow@3.1.0: {}
|
tinyrainbow@3.1.0: {}
|
||||||
|
|
||||||
|
tinyspy@4.0.4: {}
|
||||||
|
|
||||||
tldts-core@7.0.27: {}
|
tldts-core@7.0.27: {}
|
||||||
|
|
||||||
tldts@7.0.27:
|
tldts@7.0.27:
|
||||||
@@ -5968,6 +6156,27 @@ snapshots:
|
|||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
vfile-message: 4.0.3
|
vfile-message: 4.0.3
|
||||||
|
|
||||||
|
vite-node@3.2.4(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3):
|
||||||
|
dependencies:
|
||||||
|
cac: 6.7.14
|
||||||
|
debug: 4.4.3
|
||||||
|
es-module-lexer: 1.7.0
|
||||||
|
pathe: 2.0.3
|
||||||
|
vite: 6.4.1(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/node'
|
||||||
|
- jiti
|
||||||
|
- less
|
||||||
|
- lightningcss
|
||||||
|
- sass
|
||||||
|
- sass-embedded
|
||||||
|
- stylus
|
||||||
|
- sugarss
|
||||||
|
- supports-color
|
||||||
|
- terser
|
||||||
|
- tsx
|
||||||
|
- yaml
|
||||||
|
|
||||||
vite@6.4.1(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3):
|
vite@6.4.1(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.25.12
|
esbuild: 0.25.12
|
||||||
@@ -5982,6 +6191,49 @@ snapshots:
|
|||||||
tsx: 4.21.0
|
tsx: 4.21.0
|
||||||
yaml: 2.8.3
|
yaml: 2.8.3
|
||||||
|
|
||||||
|
vitest@3.2.4(@types/debug@4.1.13)(@types/node@25.5.0)(jsdom@29.0.1)(tsx@4.21.0)(yaml@2.8.3):
|
||||||
|
dependencies:
|
||||||
|
'@types/chai': 5.2.3
|
||||||
|
'@vitest/expect': 3.2.4
|
||||||
|
'@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3))
|
||||||
|
'@vitest/pretty-format': 3.2.4
|
||||||
|
'@vitest/runner': 3.2.4
|
||||||
|
'@vitest/snapshot': 3.2.4
|
||||||
|
'@vitest/spy': 3.2.4
|
||||||
|
'@vitest/utils': 3.2.4
|
||||||
|
chai: 5.3.3
|
||||||
|
debug: 4.4.3
|
||||||
|
expect-type: 1.3.0
|
||||||
|
magic-string: 0.30.21
|
||||||
|
pathe: 2.0.3
|
||||||
|
picomatch: 4.0.4
|
||||||
|
std-env: 3.10.0
|
||||||
|
tinybench: 2.9.0
|
||||||
|
tinyexec: 0.3.2
|
||||||
|
tinyglobby: 0.2.15
|
||||||
|
tinypool: 1.1.1
|
||||||
|
tinyrainbow: 2.0.0
|
||||||
|
vite: 6.4.1(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3)
|
||||||
|
vite-node: 3.2.4(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3)
|
||||||
|
why-is-node-running: 2.3.0
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/debug': 4.1.13
|
||||||
|
'@types/node': 25.5.0
|
||||||
|
jsdom: 29.0.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- jiti
|
||||||
|
- less
|
||||||
|
- lightningcss
|
||||||
|
- msw
|
||||||
|
- sass
|
||||||
|
- sass-embedded
|
||||||
|
- stylus
|
||||||
|
- sugarss
|
||||||
|
- supports-color
|
||||||
|
- terser
|
||||||
|
- tsx
|
||||||
|
- yaml
|
||||||
|
|
||||||
vitest@4.1.1(@types/node@25.5.0)(jsdom@29.0.1)(vite@6.4.1(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3)):
|
vitest@4.1.1(@types/node@25.5.0)(jsdom@29.0.1)(vite@6.4.1(@types/node@25.5.0)(tsx@4.21.0)(yaml@2.8.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/expect': 4.1.1
|
'@vitest/expect': 4.1.1
|
||||||
|
|||||||
Reference in New Issue
Block a user