Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 94f60cf0ec | |||
| f13105333a |
@@ -40,6 +40,7 @@
|
|||||||
"axios": "1.16.0",
|
"axios": "1.16.0",
|
||||||
"blueimp-load-image": "5.16.0",
|
"blueimp-load-image": "5.16.0",
|
||||||
"clsx": "2.1.1",
|
"clsx": "2.1.1",
|
||||||
|
"diff": "8.0.3",
|
||||||
"dompurify": "3.4.1",
|
"dompurify": "3.4.1",
|
||||||
"file-saver": "2.0.5",
|
"file-saver": "2.0.5",
|
||||||
"highlightjs-sap-abap": "0.3.0",
|
"highlightjs-sap-abap": "0.3.0",
|
||||||
@@ -81,7 +82,6 @@
|
|||||||
"@types/react": "18.3.12",
|
"@types/react": "18.3.12",
|
||||||
"@types/react-dom": "18.3.1",
|
"@types/react-dom": "18.3.1",
|
||||||
"@vitejs/plugin-react": "6.0.1",
|
"@vitejs/plugin-react": "6.0.1",
|
||||||
"@vitest/coverage-v8": "4.1.6",
|
|
||||||
"eslint": "9.28.0",
|
"eslint": "9.28.0",
|
||||||
"eslint-plugin-react": "7.37.5",
|
"eslint-plugin-react": "7.37.5",
|
||||||
"eslint-plugin-react-hooks": "7.0.1",
|
"eslint-plugin-react-hooks": "7.0.1",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||||
import { render, screen, fireEvent, act, cleanup } from "@testing-library/react";
|
import { render, screen, fireEvent, act } from "@testing-library/react";
|
||||||
import { MantineProvider } from "@mantine/core";
|
import { MantineProvider } from "@mantine/core";
|
||||||
|
|
||||||
// Shared, hoisted mock state so the @ai-sdk/react and "ai" module mocks (hoisted
|
// Shared, hoisted mock state so the @ai-sdk/react and "ai" module mocks (hoisted
|
||||||
@@ -140,91 +140,3 @@ describe("ChatThread — send now (#198)", () => {
|
|||||||
expect(prep({ messages: [], body: {} }).body.interrupted).toBe(false);
|
expect(prep({ messages: [], body: {} }).body.interrupted).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// The turn-end decision lives in the `onFinish` handler: given the terminal
|
|
||||||
// outcome of a turn (`isAbort` / `isDisconnect` / `isError`, or none = clean),
|
|
||||||
// it decides whether to CONTINUE (flush the next queued message) or END (leave
|
|
||||||
// the queue intact for the user), and which stop notice — if any — to show.
|
|
||||||
// `sendNow` is exercised above; these tests pin down the plain outcomes.
|
|
||||||
describe("ChatThread — turn-end decision (onFinish)", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
h.state.status = "streaming";
|
|
||||||
h.state.onFinish = null;
|
|
||||||
h.state.sendMessage.mockClear();
|
|
||||||
h.state.stop.mockClear();
|
|
||||||
h.state.transport = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Drive a fresh onFinish with the given terminal flags after queueing a
|
|
||||||
// message, and report both what the parent was told and whether the queue was
|
|
||||||
// flushed (a resend to the sendMessage spy).
|
|
||||||
function finishWith(flags: {
|
|
||||||
isAbort?: boolean;
|
|
||||||
isDisconnect?: boolean;
|
|
||||||
isError?: boolean;
|
|
||||||
}) {
|
|
||||||
// Tear down any prior render so the loop-driven "every outcome" case does
|
|
||||||
// not leave duplicate queue buttons in the DOM.
|
|
||||||
cleanup();
|
|
||||||
h.state.sendMessage.mockClear();
|
|
||||||
const { onTurnFinished } = renderThread();
|
|
||||||
// Populate the queue while the turn is streaming.
|
|
||||||
fireEvent.click(screen.getByTestId("queue-btn"));
|
|
||||||
act(() => {
|
|
||||||
h.state.onFinish?.({
|
|
||||||
message: { id: "a", role: "assistant", parts: [] },
|
|
||||||
isAbort: false,
|
|
||||||
isDisconnect: false,
|
|
||||||
isError: false,
|
|
||||||
...flags,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return { onTurnFinished };
|
|
||||||
}
|
|
||||||
|
|
||||||
it("CONTINUES — flushes the next queued message on a clean finish", () => {
|
|
||||||
finishWith({});
|
|
||||||
// Clean finish (no terminal flag): the queued message is auto-sent.
|
|
||||||
expect(h.state.sendMessage).toHaveBeenCalledWith({ text: "queued text" });
|
|
||||||
// A clean finish shows no stop notice.
|
|
||||||
expect(screen.queryByText("Response stopped.")).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("ENDS — keeps the queue intact on a user abort and shows the stopped notice", () => {
|
|
||||||
finishWith({ isAbort: true });
|
|
||||||
// A plain Stop (not the sendNow interrupt path) must NOT auto-resend: the
|
|
||||||
// queue is preserved for the user to decide.
|
|
||||||
expect(h.state.sendMessage).not.toHaveBeenCalled();
|
|
||||||
expect(screen.getByText("Response stopped.")).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("ENDS — keeps the queue intact on a disconnect and shows the connection-lost notice", () => {
|
|
||||||
finishWith({ isDisconnect: true });
|
|
||||||
expect(h.state.sendMessage).not.toHaveBeenCalled();
|
|
||||||
expect(
|
|
||||||
screen.getByText("Connection lost — the answer was interrupted."),
|
|
||||||
).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("ENDS — keeps the queue intact on a stream error (no auto-retry, no stopped notice)", () => {
|
|
||||||
finishWith({ isError: true });
|
|
||||||
// Blindly retrying after a failure would be wrong; the queue is left alone.
|
|
||||||
expect(h.state.sendMessage).not.toHaveBeenCalled();
|
|
||||||
// isError clears the neutral notice (the error banner covers this case).
|
|
||||||
expect(screen.queryByText("Response stopped.")).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("notifies the parent on EVERY terminal outcome", () => {
|
|
||||||
// The chat-list refresh / new-chat id adoption must run on success and on
|
|
||||||
// every failure path alike.
|
|
||||||
for (const flags of [
|
|
||||||
{},
|
|
||||||
{ isAbort: true },
|
|
||||||
{ isDisconnect: true },
|
|
||||||
{ isError: true },
|
|
||||||
]) {
|
|
||||||
const { onTurnFinished } = finishWith(flags);
|
|
||||||
expect(onTurnFinished).toHaveBeenCalled();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -108,10 +108,12 @@ describe("CommentListItem — suggested edit (#315)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("renders the было→стало diff and an Apply button when canEdit and not applied/resolved", () => {
|
it("renders the было→стало diff and an Apply button when canEdit and not applied/resolved", () => {
|
||||||
renderItem(suggestion(), true);
|
const { container } = renderItem(suggestion(), true);
|
||||||
// Old text appears both as the selection quote and as the struck diff row.
|
// Old text appears as the selection quote (a single unsplit Text node).
|
||||||
expect(screen.getAllByText("old wording here").length).toBeGreaterThan(0);
|
expect(screen.getAllByText("old wording here").length).toBeGreaterThan(0);
|
||||||
expect(screen.getByText("new wording here")).toBeDefined();
|
// The new line is now rendered as per-fragment spans (intraline diff, #331),
|
||||||
|
// so it is no longer a single text node — assert the concatenated content.
|
||||||
|
expect(container.textContent).toContain("new wording here");
|
||||||
// Apply button is present.
|
// Apply button is present.
|
||||||
expect(screen.getByRole("button", { name: "Apply" })).toBeDefined();
|
expect(screen.getByRole("button", { name: "Apply" })).toBeDefined();
|
||||||
// No Applied badge yet.
|
// No Applied badge yet.
|
||||||
@@ -119,9 +121,9 @@ describe("CommentListItem — suggested edit (#315)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("hides the Apply button when canEdit is false", () => {
|
it("hides the Apply button when canEdit is false", () => {
|
||||||
renderItem(suggestion(), false);
|
const { container } = renderItem(suggestion(), false);
|
||||||
// Diff still renders...
|
// Diff still renders (as per-fragment spans, #331)...
|
||||||
expect(screen.getByText("new wording here")).toBeDefined();
|
expect(container.textContent).toContain("new wording here");
|
||||||
// ...but no Apply button.
|
// ...but no Apply button.
|
||||||
expect(screen.queryByRole("button", { name: "Apply" })).toBeNull();
|
expect(screen.queryByRole("button", { name: "Apply" })).toBeNull();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Group, Text, Box, Badge, Button } from "@mantine/core";
|
import { Group, Text, Box, Badge, Button } from "@mantine/core";
|
||||||
import { AgentAvatarStack } from "@/components/ui/agent-avatar-stack.tsx";
|
import { AgentAvatarStack } from "@/components/ui/agent-avatar-stack.tsx";
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import classes from "./comment.module.css";
|
import classes from "./comment.module.css";
|
||||||
import { useAtom, useAtomValue } from "jotai";
|
import { useAtom, useAtomValue } from "jotai";
|
||||||
import { useTimeAgo } from "@/hooks/use-time-ago";
|
import { useTimeAgo } from "@/hooks/use-time-ago";
|
||||||
@@ -17,7 +17,10 @@ import {
|
|||||||
useUpdateCommentMutation,
|
useUpdateCommentMutation,
|
||||||
} from "@/features/comment/queries/comment-query";
|
} from "@/features/comment/queries/comment-query";
|
||||||
import { IComment } from "@/features/comment/types/comment.types";
|
import { IComment } from "@/features/comment/types/comment.types";
|
||||||
import { canShowApply } from "@/features/comment/utils/suggestion";
|
import {
|
||||||
|
canShowApply,
|
||||||
|
computeSuggestionDiff,
|
||||||
|
} from "@/features/comment/utils/suggestion";
|
||||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||||
import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
|
import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -54,6 +57,18 @@ function CommentListItem({
|
|||||||
const [currentUser] = useAtom(currentUserAtom);
|
const [currentUser] = useAtom(currentUserAtom);
|
||||||
const createdAtAgo = useTimeAgo(comment.createdAt);
|
const createdAtAgo = useTimeAgo(comment.createdAt);
|
||||||
|
|
||||||
|
// Intraline "before -> after" diff (#331) for a suggested edit: only the
|
||||||
|
// fragments that actually changed get emphasised inside the red/green block,
|
||||||
|
// instead of striking through / greening the whole line. Memoised on the
|
||||||
|
// (selection, suggestedText) pair so it recomputes only when they change.
|
||||||
|
const suggestionDiff = useMemo(
|
||||||
|
() =>
|
||||||
|
comment.suggestedText != null
|
||||||
|
? computeSuggestionDiff(comment.selection ?? "", comment.suggestedText)
|
||||||
|
: null,
|
||||||
|
[comment.selection, comment.suggestedText],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setContent(comment.content);
|
setContent(comment.content);
|
||||||
}, [comment]);
|
}, [comment]);
|
||||||
@@ -236,12 +251,28 @@ function CommentListItem({
|
|||||||
{!comment.parentCommentId && comment.suggestedText && (
|
{!comment.parentCommentId && comment.suggestedText && (
|
||||||
<Box className={classes.suggestionBlock}>
|
<Box className={classes.suggestionBlock}>
|
||||||
{comment.selection && (
|
{comment.selection && (
|
||||||
|
// Old line: read as removed as a whole (line-through/red); only the
|
||||||
|
// changed fragments carry the extra intraline emphasis.
|
||||||
<Text size="xs" className={classes.suggestionOld}>
|
<Text size="xs" className={classes.suggestionOld}>
|
||||||
{comment.selection}
|
{suggestionDiff?.old.map((segment, index) => (
|
||||||
|
<span
|
||||||
|
key={index}
|
||||||
|
className={segment.changed ? classes.suggestionChanged : undefined}
|
||||||
|
>
|
||||||
|
{segment.text}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
<Text size="xs" className={classes.suggestionNew}>
|
<Text size="xs" className={classes.suggestionNew}>
|
||||||
{comment.suggestedText}
|
{suggestionDiff?.new.map((segment, index) => (
|
||||||
|
<span
|
||||||
|
key={index}
|
||||||
|
className={segment.changed ? classes.suggestionChanged : undefined}
|
||||||
|
>
|
||||||
|
{segment.text}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{comment.suggestionAppliedAt ? (
|
{comment.suggestionAppliedAt ? (
|
||||||
|
|||||||
@@ -53,6 +53,21 @@
|
|||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Intraline diff (#331): the fragment that actually changed within the
|
||||||
|
red "before" / green "after" block. It inherits the surrounding red/green
|
||||||
|
framing and adds a stronger tint plus bold weight so the eye lands on the
|
||||||
|
changed letters/words (git/GitHub-style) rather than the whole line. The
|
||||||
|
container's line-through (old) / green (new) still marks the full line. */
|
||||||
|
.suggestionChanged {
|
||||||
|
/* Stronger tint of the surrounding red/green so the changed fragment pops
|
||||||
|
within the block. `currentColor` follows the parent's red (old) or green
|
||||||
|
(new) text colour. No `text-decoration` here on purpose: the old block's
|
||||||
|
inherited line-through must survive on the changed letters too. */
|
||||||
|
background: color-mix(in srgb, currentColor 22%, transparent);
|
||||||
|
border-radius: 2px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.commentEditor {
|
.commentEditor {
|
||||||
|
|
||||||
&[data-editable][data-surface="muted"] .ProseMirror:not(.focused) {
|
&[data-editable][data-surface="muted"] .ProseMirror:not(.focused) {
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { computeSuggestionDiff, Segment } from "@/features/comment/utils/suggestion";
|
||||||
|
|
||||||
|
// Reconstruct the plain string from a segment stream — the diff must be
|
||||||
|
// lossless (concatenating every fragment yields the original input).
|
||||||
|
const join = (segments: Segment[]): string =>
|
||||||
|
segments.map((s) => s.text).join("");
|
||||||
|
|
||||||
|
// The subset of segments (in order) that the UI would emphasise.
|
||||||
|
const changed = (segments: Segment[]): string[] =>
|
||||||
|
segments.filter((s) => s.changed).map((s) => s.text);
|
||||||
|
|
||||||
|
// Find the segment that contains a substring, to assert its `changed` flag.
|
||||||
|
const segmentWith = (segments: Segment[], needle: string): Segment | undefined =>
|
||||||
|
segments.find((s) => s.text.includes(needle));
|
||||||
|
|
||||||
|
describe("computeSuggestionDiff", () => {
|
||||||
|
it("highlights only the single changed letter in a one-letter edit", () => {
|
||||||
|
const { old, new: neu } = computeSuggestionDiff("заведем", "заведём");
|
||||||
|
|
||||||
|
// Lossless.
|
||||||
|
expect(join(old)).toBe("заведем");
|
||||||
|
expect(join(neu)).toBe("заведём");
|
||||||
|
|
||||||
|
// Old side: exactly the `е` is changed, the rest is common.
|
||||||
|
expect(changed(old)).toEqual(["е"]);
|
||||||
|
expect(old).toEqual([
|
||||||
|
{ text: "завед", changed: false },
|
||||||
|
{ text: "е", changed: true },
|
||||||
|
{ text: "м", changed: false },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// New side: exactly the `ё` is changed.
|
||||||
|
expect(changed(neu)).toEqual(["ё"]);
|
||||||
|
expect(neu).toEqual([
|
||||||
|
{ text: "завед", changed: false },
|
||||||
|
{ text: "ё", changed: true },
|
||||||
|
{ text: "м", changed: false },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("marks the differing words changed but keeps the shared word common", () => {
|
||||||
|
const { old, new: neu } = computeSuggestionDiff(
|
||||||
|
"привет мир",
|
||||||
|
"здравствуй мир",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Lossless.
|
||||||
|
expect(join(old)).toBe("привет мир");
|
||||||
|
expect(join(neu)).toBe("здравствуй мир");
|
||||||
|
|
||||||
|
// The shared trailing word stays common on both sides (no per-letter noise
|
||||||
|
// leaking across the differing words into `мир`).
|
||||||
|
expect(segmentWith(old, "мир")?.changed).toBe(false);
|
||||||
|
expect(segmentWith(neu, "мир")?.changed).toBe(false);
|
||||||
|
|
||||||
|
// The differing words are emphasised somewhere on each side.
|
||||||
|
expect(changed(old).length).toBeGreaterThan(0);
|
||||||
|
expect(changed(neu).length).toBeGreaterThan(0);
|
||||||
|
expect(changed(old).join("")).toContain("п"); // from `привет`
|
||||||
|
expect(changed(neu).join("")).toContain("зд"); // from `здравствуй`
|
||||||
|
|
||||||
|
// No changed fragment on either side touches the word `мир`.
|
||||||
|
expect(changed(old).some((t) => t.includes("мир"))).toBe(false);
|
||||||
|
expect(changed(neu).some((t) => t.includes("мир"))).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("marks a whole inserted word changed and leaves the old line common", () => {
|
||||||
|
const { old, new: neu } = computeSuggestionDiff("a c", "a b c");
|
||||||
|
|
||||||
|
expect(join(old)).toBe("a c");
|
||||||
|
expect(join(neu)).toBe("a b c");
|
||||||
|
|
||||||
|
// Old line has no changed fragment (nothing was removed).
|
||||||
|
expect(changed(old)).toEqual([]);
|
||||||
|
// The inserted word is the only changed fragment on the new side.
|
||||||
|
expect(neu).toContainEqual({ text: "b ", changed: true });
|
||||||
|
expect(changed(neu)).toEqual(["b "]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("marks a whole deleted word changed and leaves the new line common", () => {
|
||||||
|
const { old, new: neu } = computeSuggestionDiff("a b c", "a c");
|
||||||
|
|
||||||
|
expect(join(old)).toBe("a b c");
|
||||||
|
expect(join(neu)).toBe("a c");
|
||||||
|
|
||||||
|
// The deleted word is the only changed fragment on the old side.
|
||||||
|
expect(old).toContainEqual({ text: "b ", changed: true });
|
||||||
|
expect(changed(old)).toEqual(["b "]);
|
||||||
|
// New line has no changed fragment (nothing was added).
|
||||||
|
expect(changed(neu)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("marks everything common for identical strings", () => {
|
||||||
|
const { old, new: neu } = computeSuggestionDiff("hello", "hello");
|
||||||
|
|
||||||
|
expect(old).toEqual([{ text: "hello", changed: false }]);
|
||||||
|
expect(neu).toEqual([{ text: "hello", changed: false }]);
|
||||||
|
expect(changed(old)).toEqual([]);
|
||||||
|
expect(changed(neu)).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { diffWordsWithSpace, diffChars } from "diff";
|
||||||
import { IComment } from "@/features/comment/types/comment.types";
|
import { IComment } from "@/features/comment/types/comment.types";
|
||||||
|
|
||||||
// Whether the suggested-edit (#315) "Apply" button should be shown for a
|
// Whether the suggested-edit (#315) "Apply" button should be shown for a
|
||||||
@@ -12,3 +13,105 @@ export function canShowApply(comment: IComment, canEdit?: boolean): boolean {
|
|||||||
!comment.parentCommentId,
|
!comment.parentCommentId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One contiguous run of text within a suggestion's "before" or "after" line.
|
||||||
|
// `changed` marks the fragment that actually differs from the other side, so
|
||||||
|
// the UI can emphasise only the intraline delta (git/GitHub-style) instead of
|
||||||
|
// the whole line.
|
||||||
|
export interface Segment {
|
||||||
|
text: string;
|
||||||
|
changed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A pure "before -> after" intraline diff (#331): the old line split into
|
||||||
|
// common vs. removed-and-changed fragments, and the new line split into common
|
||||||
|
// vs. added-and-changed fragments. Concatenating each side's `text` reproduces
|
||||||
|
// the original strings.
|
||||||
|
export interface SuggestionDiff {
|
||||||
|
old: Segment[];
|
||||||
|
new: Segment[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push a segment, coalescing runs of the same `changed` flag on the same side
|
||||||
|
// so the render emits as few spans as possible and tests stay predictable.
|
||||||
|
function pushSegment(segments: Segment[], text: string, changed: boolean): void {
|
||||||
|
if (text === "") return;
|
||||||
|
const last = segments[segments.length - 1];
|
||||||
|
if (last && last.changed === changed) {
|
||||||
|
last.text += text;
|
||||||
|
} else {
|
||||||
|
segments.push({ text, changed });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute an intraline diff between the old `selection` and the new
|
||||||
|
// `suggestedText` of a suggestion. PURE — no React, no DOM, no I/O.
|
||||||
|
//
|
||||||
|
// Hybrid word + char algorithm (per #331):
|
||||||
|
// 1. `diffWordsWithSpace` yields word-granular parts [{value, added, removed}].
|
||||||
|
// 2. An ADJACENT removed+added pair (a word replacement) is refined with
|
||||||
|
// `diffChars`: shared characters stay common, differing characters are
|
||||||
|
// marked `changed` on their respective side. This is what keeps a
|
||||||
|
// one-letter edit (заведем -> заведём) from highlighting the whole word.
|
||||||
|
// 3. A lone `added` (insertion) or lone `removed` (deletion) marks the whole
|
||||||
|
// fragment `changed`.
|
||||||
|
// 4. An unchanged part is `common` on both sides.
|
||||||
|
//
|
||||||
|
// Rejected alternatives: pure `diffChars` is noisy on word swaps; pure
|
||||||
|
// `diffWordsWithSpace` highlights the whole word rather than the changed letter.
|
||||||
|
export function computeSuggestionDiff(
|
||||||
|
oldStr: string,
|
||||||
|
newStr: string,
|
||||||
|
): SuggestionDiff {
|
||||||
|
const oldSegments: Segment[] = [];
|
||||||
|
const newSegments: Segment[] = [];
|
||||||
|
|
||||||
|
const parts = diffWordsWithSpace(oldStr, newStr);
|
||||||
|
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
const part = parts[i];
|
||||||
|
const next = parts[i + 1];
|
||||||
|
|
||||||
|
// A word replacement: a removed part immediately followed by an added part
|
||||||
|
// (or the reverse). Refine it character-by-character so only the differing
|
||||||
|
// letters are highlighted while shared letters stay common.
|
||||||
|
const isReplacementPair =
|
||||||
|
next &&
|
||||||
|
((part.removed && next.added) || (part.added && next.removed));
|
||||||
|
|
||||||
|
if (isReplacementPair) {
|
||||||
|
const removedPart = part.removed ? part : next;
|
||||||
|
const addedPart = part.added ? part : next;
|
||||||
|
|
||||||
|
const charParts = diffChars(removedPart.value, addedPart.value);
|
||||||
|
for (const cp of charParts) {
|
||||||
|
if (cp.added) {
|
||||||
|
pushSegment(newSegments, cp.value, true);
|
||||||
|
} else if (cp.removed) {
|
||||||
|
pushSegment(oldSegments, cp.value, true);
|
||||||
|
} else {
|
||||||
|
// Shared character: common on both sides.
|
||||||
|
pushSegment(oldSegments, cp.value, false);
|
||||||
|
pushSegment(newSegments, cp.value, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i++; // consume the paired part as well
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (part.added) {
|
||||||
|
// Lone insertion: only present in the new line, wholly changed.
|
||||||
|
pushSegment(newSegments, part.value, true);
|
||||||
|
} else if (part.removed) {
|
||||||
|
// Lone deletion: only present in the old line, wholly changed.
|
||||||
|
pushSegment(oldSegments, part.value, true);
|
||||||
|
} else {
|
||||||
|
// Unchanged: common on both sides.
|
||||||
|
pushSegment(oldSegments, part.value, false);
|
||||||
|
pushSegment(newSegments, part.value, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { old: oldSegments, new: newSegments };
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,22 +13,5 @@ export default defineConfig({
|
|||||||
environment: 'jsdom',
|
environment: 'jsdom',
|
||||||
globals: true,
|
globals: true,
|
||||||
setupFiles: ['./vitest.setup.ts'],
|
setupFiles: ['./vitest.setup.ts'],
|
||||||
// Coverage gate (issue #324). v8 provider (not istanbul) so ESM barrels
|
|
||||||
// like `@docmost/editor-ext` are not re-parsed/instrumented. Thresholds are
|
|
||||||
// set a few points below the level measured on develop, scoped to the files
|
|
||||||
// the suite exercises (`all: false`) rather than the whole app, so the gate
|
|
||||||
// passes today but fails on a genuine coverage regression.
|
|
||||||
coverage: {
|
|
||||||
enabled: true,
|
|
||||||
provider: 'v8',
|
|
||||||
reporter: ['text-summary', 'text'],
|
|
||||||
all: false,
|
|
||||||
thresholds: {
|
|
||||||
statements: 55,
|
|
||||||
branches: 53,
|
|
||||||
functions: 44,
|
|
||||||
lines: 55,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -132,62 +132,6 @@ export async function createUser(
|
|||||||
return { id: row.id as string };
|
return { id: row.id as string };
|
||||||
}
|
}
|
||||||
|
|
||||||
// The default group every workspace has; `groupUserRepo.addUserToDefaultGroup`
|
|
||||||
// (invoked by acceptInvitation) looks it up by `isDefault = true`, so a
|
|
||||||
// workspace under test must have exactly one for the accept path to complete.
|
|
||||||
export async function createDefaultGroup(
|
|
||||||
db: Kysely<any>,
|
|
||||||
workspaceId: string,
|
|
||||||
overrides: { name?: string } = {},
|
|
||||||
): Promise<{ id: string }> {
|
|
||||||
const id = randomUUID();
|
|
||||||
const suffix = shortId(id);
|
|
||||||
const row = await db
|
|
||||||
.insertInto('groups')
|
|
||||||
.values({
|
|
||||||
id,
|
|
||||||
// name is unique per workspace + NOT NULL.
|
|
||||||
name: overrides.name ?? `group-${suffix}`,
|
|
||||||
isDefault: true,
|
|
||||||
workspaceId,
|
|
||||||
})
|
|
||||||
.returning(['id'])
|
|
||||||
.executeTakeFirstOrThrow();
|
|
||||||
return { id: row.id as string };
|
|
||||||
}
|
|
||||||
|
|
||||||
// A pending workspace invitation. `role`/`token` are NOT NULL; `groupIds` is a
|
|
||||||
// nullable uuid[] and `invitedById` a nullable FK to users. Returns the fields a
|
|
||||||
// spec needs to drive acceptInvitation (id + token + the invited email).
|
|
||||||
export async function createInvitation(
|
|
||||||
db: Kysely<any>,
|
|
||||||
args: {
|
|
||||||
workspaceId: string;
|
|
||||||
email: string;
|
|
||||||
invitedById?: string | null;
|
|
||||||
role?: string;
|
|
||||||
token?: string;
|
|
||||||
groupIds?: string[] | null;
|
|
||||||
},
|
|
||||||
): Promise<{ id: string; token: string; email: string }> {
|
|
||||||
const id = randomUUID();
|
|
||||||
const token = args.token ?? `tok-${shortId(id)}`;
|
|
||||||
const row = await db
|
|
||||||
.insertInto('workspaceInvitations')
|
|
||||||
.values({
|
|
||||||
id,
|
|
||||||
email: args.email,
|
|
||||||
role: args.role ?? 'member',
|
|
||||||
token,
|
|
||||||
groupIds: (args.groupIds ?? null) as any,
|
|
||||||
invitedById: args.invitedById ?? null,
|
|
||||||
workspaceId: args.workspaceId,
|
|
||||||
})
|
|
||||||
.returning(['id'])
|
|
||||||
.executeTakeFirstOrThrow();
|
|
||||||
return { id: row.id as string, token, email: args.email };
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createSpace(
|
export async function createSpace(
|
||||||
db: Kysely<any>,
|
db: Kysely<any>,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
|
|||||||
@@ -1,218 +0,0 @@
|
|||||||
import { BadRequestException } from '@nestjs/common';
|
|
||||||
import { Kysely } from 'kysely';
|
|
||||||
import { Workspace } from '@docmost/db/types/entity.types';
|
|
||||||
import { UserRepo } from '@docmost/db/repos/user/user.repo';
|
|
||||||
import { GroupRepo } from '@docmost/db/repos/group/group.repo';
|
|
||||||
import { GroupUserRepo } from '@docmost/db/repos/group/group-user.repo';
|
|
||||||
import { WorkspaceInvitationService } from 'src/core/workspace/services/workspace-invitation.service';
|
|
||||||
import {
|
|
||||||
getTestDb,
|
|
||||||
destroyTestDb,
|
|
||||||
createWorkspace,
|
|
||||||
createUser,
|
|
||||||
createDefaultGroup,
|
|
||||||
createInvitation,
|
|
||||||
} from './db';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* acceptInvitation atomicity (issue #324, tail of #244).
|
|
||||||
*
|
|
||||||
* acceptInvitation() reads the invitation OUTSIDE the transaction, then inside a
|
|
||||||
* single tx: inserts the invited user, adds them to the default group, and
|
|
||||||
* deletes the invitation. Two accepts of the SAME invitation therefore race to
|
|
||||||
* insert a user with the same (email, workspaceId) — which the
|
|
||||||
* `users_email_workspace_id_unique` constraint forbids. The service catches that
|
|
||||||
* violation and reports "Invitation already accepted".
|
|
||||||
*
|
|
||||||
* These specs pin the INVARIANT that path protects: no matter how many times the
|
|
||||||
* invitation is accepted (concurrently or repeatedly), the workspace ends up
|
|
||||||
* with exactly ONE membership for the invited email and the invitation is
|
|
||||||
* consumed exactly once — never a duplicate user and never a half-applied state.
|
|
||||||
*
|
|
||||||
* The service is wired with the REAL repos (UserRepo / GroupRepo / GroupUserRepo)
|
|
||||||
* against the test Kysely; only the peripheral collaborators that acceptInvitation
|
|
||||||
* touches AFTER the transaction (mail, session token, billing, audit, env) are
|
|
||||||
* stubbed, so the exercised DB write path is the production one.
|
|
||||||
*/
|
|
||||||
describe('WorkspaceInvitationService.acceptInvitation atomicity [integration]', () => {
|
|
||||||
let db: Kysely<any>;
|
|
||||||
let service: WorkspaceInvitationService;
|
|
||||||
|
|
||||||
// Count the memberships (user rows) for an email within a workspace — the
|
|
||||||
// quantity the atomicity guarantee is about.
|
|
||||||
async function membershipCount(
|
|
||||||
workspaceId: string,
|
|
||||||
email: string,
|
|
||||||
): Promise<number> {
|
|
||||||
const rows = await db
|
|
||||||
.selectFrom('users')
|
|
||||||
.select('id')
|
|
||||||
.where('workspaceId', '=', workspaceId)
|
|
||||||
.where('email', '=', email.toLowerCase())
|
|
||||||
.execute();
|
|
||||||
return rows.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function invitationExists(invitationId: string): Promise<boolean> {
|
|
||||||
const row = await db
|
|
||||||
.selectFrom('workspaceInvitations')
|
|
||||||
.select('id')
|
|
||||||
.where('id', '=', invitationId)
|
|
||||||
.executeTakeFirst();
|
|
||||||
return !!row;
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
db = getTestDb();
|
|
||||||
|
|
||||||
const userRepo = new UserRepo(db as any);
|
|
||||||
const groupRepo = new GroupRepo(db as any);
|
|
||||||
const groupUserRepo = new GroupUserRepo(db as any, groupRepo, userRepo);
|
|
||||||
|
|
||||||
// Collaborators used only on the post-commit success tail; safe to stub.
|
|
||||||
const mailService = { sendToQueue: jest.fn().mockResolvedValue(undefined) };
|
|
||||||
const domainService = {} as any;
|
|
||||||
const tokenService = {} as any;
|
|
||||||
const sessionService = {
|
|
||||||
createSessionAndToken: jest.fn().mockResolvedValue('test-auth-token'),
|
|
||||||
};
|
|
||||||
const billingQueue = { add: jest.fn().mockResolvedValue(undefined) };
|
|
||||||
const environmentService = { isCloud: () => false };
|
|
||||||
const auditService = { log: jest.fn() };
|
|
||||||
|
|
||||||
service = new WorkspaceInvitationService(
|
|
||||||
userRepo,
|
|
||||||
groupUserRepo,
|
|
||||||
mailService as any,
|
|
||||||
domainService,
|
|
||||||
tokenService,
|
|
||||||
sessionService as any,
|
|
||||||
db as any,
|
|
||||||
billingQueue as any,
|
|
||||||
environmentService as any,
|
|
||||||
auditService as any,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await destroyTestDb();
|
|
||||||
});
|
|
||||||
|
|
||||||
// A workspace with its default group, an inviter, and a pending invitation.
|
|
||||||
async function seedInvite(): Promise<{
|
|
||||||
workspace: Workspace;
|
|
||||||
invitationId: string;
|
|
||||||
token: string;
|
|
||||||
email: string;
|
|
||||||
}> {
|
|
||||||
const { id: workspaceId } = await createWorkspace(db);
|
|
||||||
await createDefaultGroup(db, workspaceId);
|
|
||||||
const inviter = await createUser(db, workspaceId);
|
|
||||||
// Distinct address per invite so specs never collide across the suite.
|
|
||||||
const email = `invitee-${workspaceId.slice(0, 8)}@example.test`;
|
|
||||||
const invite = await createInvitation(db, {
|
|
||||||
workspaceId,
|
|
||||||
email,
|
|
||||||
invitedById: inviter.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
// acceptInvitation only reads id/hostname/enforceSso/emailDomains/enforceMfa
|
|
||||||
// off the workspace; a minimal plain object is sufficient.
|
|
||||||
const workspace = {
|
|
||||||
id: workspaceId,
|
|
||||||
hostname: `host-${workspaceId.slice(0, 8)}`,
|
|
||||||
enforceSso: false,
|
|
||||||
enforceMfa: false,
|
|
||||||
emailDomains: [] as string[],
|
|
||||||
} as unknown as Workspace;
|
|
||||||
|
|
||||||
return { workspace, invitationId: invite.id, token: invite.token, email };
|
|
||||||
}
|
|
||||||
|
|
||||||
it('concurrent accepts create a single membership and consume the invitation once', async () => {
|
|
||||||
const { workspace, invitationId, token, email } = await seedInvite();
|
|
||||||
|
|
||||||
const dto = { invitationId, token, name: 'Invited User', password: 'password123' };
|
|
||||||
|
|
||||||
// Fire two accepts of the SAME invitation at once. They race to insert the
|
|
||||||
// same (email, workspaceId); the unique constraint lets exactly one win.
|
|
||||||
const results = await Promise.allSettled([
|
|
||||||
service.acceptInvitation({ ...dto }, workspace),
|
|
||||||
service.acceptInvitation({ ...dto }, workspace),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const fulfilled = results.filter((r) => r.status === 'fulfilled');
|
|
||||||
const rejected = results.filter(
|
|
||||||
(r): r is PromiseRejectedResult => r.status === 'rejected',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Exactly one accept succeeds; the other is rejected.
|
|
||||||
expect(fulfilled).toHaveLength(1);
|
|
||||||
expect(rejected).toHaveLength(1);
|
|
||||||
|
|
||||||
// The loser fails via the caught unique-constraint path with the specific
|
|
||||||
// "already accepted" message — not a half-state / generic failure.
|
|
||||||
expect(rejected[0].reason).toBeInstanceOf(BadRequestException);
|
|
||||||
expect(rejected[0].reason.message).toBe('Invitation already accepted');
|
|
||||||
|
|
||||||
// Invariant: exactly one membership, and the invitation is gone.
|
|
||||||
expect(await membershipCount(workspace.id, email)).toBe(1);
|
|
||||||
expect(await invitationExists(invitationId)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('a repeated (sequential) accept does not create a duplicate membership', async () => {
|
|
||||||
const { workspace, invitationId, token, email } = await seedInvite();
|
|
||||||
const dto = { invitationId, token, name: 'Invited User', password: 'password123' };
|
|
||||||
|
|
||||||
// First accept succeeds and returns an auth token.
|
|
||||||
const first = await service.acceptInvitation({ ...dto }, workspace);
|
|
||||||
expect(first?.authToken).toBe('test-auth-token');
|
|
||||||
expect(await membershipCount(workspace.id, email)).toBe(1);
|
|
||||||
expect(await invitationExists(invitationId)).toBe(false);
|
|
||||||
|
|
||||||
// Re-accepting the (now consumed) invitation must be rejected and must NOT
|
|
||||||
// add a second membership. The invitation row is gone, so this hits the
|
|
||||||
// "Invitation not found" guard rather than the unique-constraint path.
|
|
||||||
await expect(
|
|
||||||
service.acceptInvitation({ ...dto }, workspace),
|
|
||||||
).rejects.toBeInstanceOf(BadRequestException);
|
|
||||||
|
|
||||||
expect(await membershipCount(workspace.id, email)).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('the single created membership is added to the default group (no partial state)', async () => {
|
|
||||||
const { workspace, invitationId, token, email } = await seedInvite();
|
|
||||||
const dto = { invitationId, token, name: 'Invited User', password: 'password123' };
|
|
||||||
|
|
||||||
await Promise.allSettled([
|
|
||||||
service.acceptInvitation({ ...dto }, workspace),
|
|
||||||
service.acceptInvitation({ ...dto }, workspace),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Resolve the one surviving user and assert the whole tx applied: they exist
|
|
||||||
// AND are in the workspace default group (the mid-transaction step), proving
|
|
||||||
// the winning accept committed as a whole rather than leaving a torn state.
|
|
||||||
const user = await db
|
|
||||||
.selectFrom('users')
|
|
||||||
.select(['id'])
|
|
||||||
.where('workspaceId', '=', workspace.id)
|
|
||||||
.where('email', '=', email.toLowerCase())
|
|
||||||
.executeTakeFirstOrThrow();
|
|
||||||
|
|
||||||
const defaultGroup = await db
|
|
||||||
.selectFrom('groups')
|
|
||||||
.select(['id'])
|
|
||||||
.where('workspaceId', '=', workspace.id)
|
|
||||||
.where('isDefault', '=', true)
|
|
||||||
.executeTakeFirstOrThrow();
|
|
||||||
|
|
||||||
const membership = await db
|
|
||||||
.selectFrom('groupUsers')
|
|
||||||
.select(['userId'])
|
|
||||||
.where('groupId', '=', defaultGroup.id)
|
|
||||||
.where('userId', '=', user.id)
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
expect(membership).toHaveLength(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -13,9 +13,5 @@
|
|||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"marked": "17.0.5"
|
"marked": "17.0.5"
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@vitest/coverage-v8": "4.1.6",
|
|
||||||
"vitest": "4.1.6"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,21 +5,5 @@ export default defineConfig({
|
|||||||
environment: "jsdom",
|
environment: "jsdom",
|
||||||
globals: true,
|
globals: true,
|
||||||
include: ["src/**/*.{test,spec}.ts"],
|
include: ["src/**/*.{test,spec}.ts"],
|
||||||
// Coverage gate (issue #324). v8 provider avoids the istanbul AST-rewrite
|
|
||||||
// that broke on this package's ESM barrel. Thresholds sit a few points
|
|
||||||
// below the level measured on develop, over the files the suite exercises
|
|
||||||
// (`all: false`), so the gate passes today and catches a real regression.
|
|
||||||
coverage: {
|
|
||||||
enabled: true,
|
|
||||||
provider: "v8",
|
|
||||||
reporter: ["text-summary", "text"],
|
|
||||||
all: false,
|
|
||||||
thresholds: {
|
|
||||||
statements: 54,
|
|
||||||
branches: 44,
|
|
||||||
functions: 60,
|
|
||||||
lines: 54,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -38,7 +38,6 @@
|
|||||||
"@docmost/editor-ext": "workspace:*",
|
"@docmost/editor-ext": "workspace:*",
|
||||||
"@types/jsdom": "^21.1.7",
|
"@types/jsdom": "^21.1.7",
|
||||||
"@types/node": "^20.0.0",
|
"@types/node": "^20.0.0",
|
||||||
"@vitest/coverage-v8": "4.1.6",
|
|
||||||
"fast-check": "^4.8.0",
|
"fast-check": "^4.8.0",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"vitest": "4.1.6"
|
"vitest": "4.1.6"
|
||||||
|
|||||||
@@ -18,25 +18,6 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
test: {
|
test: {
|
||||||
environment: 'node',
|
environment: 'node',
|
||||||
// Coverage gate (issue #324). The v8 provider is used deliberately: the
|
|
||||||
// istanbul provider instruments sources by rewriting their AST, which broke
|
|
||||||
// on the ESM `@docmost/editor-ext` barrel import; v8 collects native
|
|
||||||
// coverage from the runtime and never re-parses ESM, so it sidesteps that.
|
|
||||||
// Thresholds are calibrated a few points BELOW the level measured on
|
|
||||||
// develop so the gate passes today but fails on a real regression. Numbers
|
|
||||||
// reflect the files actually exercised by the suite (`all: false`).
|
|
||||||
coverage: {
|
|
||||||
enabled: true,
|
|
||||||
provider: 'v8',
|
|
||||||
reporter: ['text-summary', 'text'],
|
|
||||||
all: false,
|
|
||||||
thresholds: {
|
|
||||||
statements: 88,
|
|
||||||
branches: 75,
|
|
||||||
functions: 72,
|
|
||||||
lines: 88,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// Runtime suites. The `.test.ts` glob deliberately EXCLUDES the type-only
|
// Runtime suites. The `.test.ts` glob deliberately EXCLUDES the type-only
|
||||||
// contract file (`*.test-d.ts`), which is enforced by the typecheck pass
|
// contract file (`*.test-d.ts`), which is enforced by the typecheck pass
|
||||||
// below instead — so the 35 runtime suites are never typechecked.
|
// below instead — so the 35 runtime suites are never typechecked.
|
||||||
|
|||||||
Generated
+8
-168
@@ -335,6 +335,9 @@ importers:
|
|||||||
clsx:
|
clsx:
|
||||||
specifier: 2.1.1
|
specifier: 2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
|
diff:
|
||||||
|
specifier: 8.0.3
|
||||||
|
version: 8.0.3
|
||||||
dompurify:
|
dompurify:
|
||||||
specifier: 3.4.1
|
specifier: 3.4.1
|
||||||
version: 3.4.1
|
version: 3.4.1
|
||||||
@@ -453,9 +456,6 @@ importers:
|
|||||||
'@vitejs/plugin-react':
|
'@vitejs/plugin-react':
|
||||||
specifier: 6.0.1
|
specifier: 6.0.1
|
||||||
version: 6.0.1(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))
|
version: 6.0.1(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))
|
||||||
'@vitest/coverage-v8':
|
|
||||||
specifier: 4.1.6
|
|
||||||
version: 4.1.6(vitest@4.1.6)
|
|
||||||
eslint:
|
eslint:
|
||||||
specifier: 9.28.0
|
specifier: 9.28.0
|
||||||
version: 9.28.0(jiti@2.4.2)
|
version: 9.28.0(jiti@2.4.2)
|
||||||
@@ -500,7 +500,7 @@ importers:
|
|||||||
version: 8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)
|
version: 8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)
|
||||||
vitest:
|
vitest:
|
||||||
specifier: 4.1.6
|
specifier: 4.1.6
|
||||||
version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/coverage-v8@4.1.6)(happy-dom@20.8.9)(jsdom@25.0.0)(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))
|
version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(happy-dom@20.8.9)(jsdom@25.0.0)(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))
|
||||||
|
|
||||||
apps/server:
|
apps/server:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -889,13 +889,6 @@ importers:
|
|||||||
marked:
|
marked:
|
||||||
specifier: 17.0.5
|
specifier: 17.0.5
|
||||||
version: 17.0.5
|
version: 17.0.5
|
||||||
devDependencies:
|
|
||||||
'@vitest/coverage-v8':
|
|
||||||
specifier: 4.1.6
|
|
||||||
version: 4.1.6(vitest@4.1.6)
|
|
||||||
vitest:
|
|
||||||
specifier: 4.1.6
|
|
||||||
version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(@vitest/coverage-v8@4.1.6)(happy-dom@20.8.9)(jsdom@27.4.0(@noble/hashes@2.0.1))(vite@8.0.5(@types/node@25.5.0)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))
|
|
||||||
|
|
||||||
packages/git-sync:
|
packages/git-sync:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -948,9 +941,6 @@ importers:
|
|||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^20.0.0
|
specifier: ^20.0.0
|
||||||
version: 20.19.43
|
version: 20.19.43
|
||||||
'@vitest/coverage-v8':
|
|
||||||
specifier: 4.1.6
|
|
||||||
version: 4.1.6(vitest@4.1.6)
|
|
||||||
fast-check:
|
fast-check:
|
||||||
specifier: ^4.8.0
|
specifier: ^4.8.0
|
||||||
version: 4.8.0
|
version: 4.8.0
|
||||||
@@ -959,7 +949,7 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: 4.1.6
|
specifier: 4.1.6
|
||||||
version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.6)(happy-dom@20.8.9)(jsdom@25.0.0)(vite@8.0.5(@types/node@20.19.43)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))
|
version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(happy-dom@20.8.9)(jsdom@25.0.0)(vite@8.0.5(@types/node@20.19.43)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))
|
||||||
|
|
||||||
packages/mcp:
|
packages/mcp:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1513,18 +1503,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
|
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@babel/helper-string-parser@7.29.7':
|
|
||||||
resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
|
|
||||||
'@babel/helper-validator-identifier@7.28.5':
|
'@babel/helper-validator-identifier@7.28.5':
|
||||||
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
|
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@babel/helper-validator-identifier@7.29.7':
|
|
||||||
resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
|
|
||||||
'@babel/helper-validator-option@7.27.1':
|
'@babel/helper-validator-option@7.27.1':
|
||||||
resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
|
resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@@ -1547,11 +1529,6 @@ packages:
|
|||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@babel/parser@7.29.7':
|
|
||||||
resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==}
|
|
||||||
engines: {node: '>=6.0.0'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
'@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.23.3':
|
'@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.23.3':
|
||||||
resolution: {integrity: sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==}
|
resolution: {integrity: sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@@ -2041,17 +2018,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
|
resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@babel/types@7.29.7':
|
|
||||||
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
|
|
||||||
engines: {node: '>=6.9.0'}
|
|
||||||
|
|
||||||
'@bcoe/v8-coverage@0.2.3':
|
'@bcoe/v8-coverage@0.2.3':
|
||||||
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
|
resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
|
||||||
|
|
||||||
'@bcoe/v8-coverage@1.0.2':
|
|
||||||
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
|
|
||||||
'@borewit/text-codec@0.2.1':
|
'@borewit/text-codec@0.2.1':
|
||||||
resolution: {integrity: sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==}
|
resolution: {integrity: sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==}
|
||||||
|
|
||||||
@@ -5487,15 +5456,6 @@ packages:
|
|||||||
babel-plugin-react-compiler:
|
babel-plugin-react-compiler:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@vitest/coverage-v8@4.1.6':
|
|
||||||
resolution: {integrity: sha512-36l628fQ/9a/8ihy97eOtEnvWQEdqULQOJtcaxtoNq0G1w3Mxd4szSahOaMM9/NGyZ+hyKcMtIW/WIxq0XQViQ==}
|
|
||||||
peerDependencies:
|
|
||||||
'@vitest/browser': 4.1.6
|
|
||||||
vitest: 4.1.6
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@vitest/browser':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@vitest/expect@4.1.6':
|
'@vitest/expect@4.1.6':
|
||||||
resolution: {integrity: sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==}
|
resolution: {integrity: sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==}
|
||||||
|
|
||||||
@@ -5781,9 +5741,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
ast-v8-to-istanbul@1.0.4:
|
|
||||||
resolution: {integrity: sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==}
|
|
||||||
|
|
||||||
async-lock@1.4.1:
|
async-lock@1.4.1:
|
||||||
resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==}
|
resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==}
|
||||||
|
|
||||||
@@ -7711,10 +7668,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==}
|
resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
istanbul-reports@3.2.0:
|
|
||||||
resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
|
|
||||||
iterare@1.2.1:
|
iterare@1.2.1:
|
||||||
resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==}
|
resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -7940,9 +7893,6 @@ packages:
|
|||||||
js-tiktoken@1.0.21:
|
js-tiktoken@1.0.21:
|
||||||
resolution: {integrity: sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==}
|
resolution: {integrity: sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==}
|
||||||
|
|
||||||
js-tokens@10.0.0:
|
|
||||||
resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==}
|
|
||||||
|
|
||||||
js-tokens@4.0.0:
|
js-tokens@4.0.0:
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
|
|
||||||
@@ -8388,9 +8338,6 @@ packages:
|
|||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||||
|
|
||||||
magicast@0.5.3:
|
|
||||||
resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==}
|
|
||||||
|
|
||||||
make-dir@2.1.0:
|
make-dir@2.1.0:
|
||||||
resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==}
|
resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -11699,12 +11646,8 @@ snapshots:
|
|||||||
|
|
||||||
'@babel/helper-string-parser@7.27.1': {}
|
'@babel/helper-string-parser@7.27.1': {}
|
||||||
|
|
||||||
'@babel/helper-string-parser@7.29.7': {}
|
|
||||||
|
|
||||||
'@babel/helper-validator-identifier@7.28.5': {}
|
'@babel/helper-validator-identifier@7.28.5': {}
|
||||||
|
|
||||||
'@babel/helper-validator-identifier@7.29.7': {}
|
|
||||||
|
|
||||||
'@babel/helper-validator-option@7.27.1': {}
|
'@babel/helper-validator-option@7.27.1': {}
|
||||||
|
|
||||||
'@babel/helper-wrap-function@7.22.20':
|
'@babel/helper-wrap-function@7.22.20':
|
||||||
@@ -11726,10 +11669,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 7.28.5
|
'@babel/types': 7.28.5
|
||||||
|
|
||||||
'@babel/parser@7.29.7':
|
|
||||||
dependencies:
|
|
||||||
'@babel/types': 7.29.7
|
|
||||||
|
|
||||||
'@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.23.3(@babel/core@7.28.5)':
|
'@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.23.3(@babel/core@7.28.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
@@ -12335,15 +12274,8 @@ snapshots:
|
|||||||
'@babel/helper-string-parser': 7.27.1
|
'@babel/helper-string-parser': 7.27.1
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
'@babel/helper-validator-identifier': 7.28.5
|
||||||
|
|
||||||
'@babel/types@7.29.7':
|
|
||||||
dependencies:
|
|
||||||
'@babel/helper-string-parser': 7.29.7
|
|
||||||
'@babel/helper-validator-identifier': 7.29.7
|
|
||||||
|
|
||||||
'@bcoe/v8-coverage@0.2.3': {}
|
'@bcoe/v8-coverage@0.2.3': {}
|
||||||
|
|
||||||
'@bcoe/v8-coverage@1.0.2': {}
|
|
||||||
|
|
||||||
'@borewit/text-codec@0.2.1': {}
|
'@borewit/text-codec@0.2.1': {}
|
||||||
|
|
||||||
'@braintree/sanitize-url@6.0.2': {}
|
'@braintree/sanitize-url@6.0.2': {}
|
||||||
@@ -13327,7 +13259,7 @@ snapshots:
|
|||||||
'@jridgewell/trace-mapping@0.3.31':
|
'@jridgewell/trace-mapping@0.3.31':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.0
|
||||||
|
|
||||||
'@jridgewell/trace-mapping@0.3.9':
|
'@jridgewell/trace-mapping@0.3.9':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -16002,20 +15934,6 @@ snapshots:
|
|||||||
'@rolldown/pluginutils': 1.0.0-rc.7
|
'@rolldown/pluginutils': 1.0.0-rc.7
|
||||||
vite: 8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)
|
vite: 8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)
|
||||||
|
|
||||||
'@vitest/coverage-v8@4.1.6(vitest@4.1.6)':
|
|
||||||
dependencies:
|
|
||||||
'@bcoe/v8-coverage': 1.0.2
|
|
||||||
'@vitest/utils': 4.1.6
|
|
||||||
ast-v8-to-istanbul: 1.0.4
|
|
||||||
istanbul-lib-coverage: 3.2.2
|
|
||||||
istanbul-lib-report: 3.0.1
|
|
||||||
istanbul-reports: 3.2.0
|
|
||||||
magicast: 0.5.3
|
|
||||||
obug: 2.1.1
|
|
||||||
std-env: 4.1.0
|
|
||||||
tinyrainbow: 3.1.0
|
|
||||||
vitest: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/coverage-v8@4.1.6)(happy-dom@20.8.9)(jsdom@25.0.0)(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))
|
|
||||||
|
|
||||||
'@vitest/expect@4.1.6':
|
'@vitest/expect@4.1.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@standard-schema/spec': 1.1.0
|
'@standard-schema/spec': 1.1.0
|
||||||
@@ -16041,14 +15959,6 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vite: 8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)
|
vite: 8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)
|
||||||
|
|
||||||
'@vitest/mocker@4.1.6(vite@8.0.5(@types/node@25.5.0)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))':
|
|
||||||
dependencies:
|
|
||||||
'@vitest/spy': 4.1.6
|
|
||||||
estree-walker: 3.0.3
|
|
||||||
magic-string: 0.30.21
|
|
||||||
optionalDependencies:
|
|
||||||
vite: 8.0.5(@types/node@25.5.0)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)
|
|
||||||
|
|
||||||
'@vitest/pretty-format@4.1.6':
|
'@vitest/pretty-format@4.1.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
tinyrainbow: 3.1.0
|
tinyrainbow: 3.1.0
|
||||||
@@ -16361,12 +16271,6 @@ snapshots:
|
|||||||
|
|
||||||
assertion-error@2.0.1: {}
|
assertion-error@2.0.1: {}
|
||||||
|
|
||||||
ast-v8-to-istanbul@1.0.4:
|
|
||||||
dependencies:
|
|
||||||
'@jridgewell/trace-mapping': 0.3.31
|
|
||||||
estree-walker: 3.0.3
|
|
||||||
js-tokens: 10.0.0
|
|
||||||
|
|
||||||
async-lock@1.4.1: {}
|
async-lock@1.4.1: {}
|
||||||
|
|
||||||
async-mutex@0.5.0:
|
async-mutex@0.5.0:
|
||||||
@@ -18586,11 +18490,6 @@ snapshots:
|
|||||||
html-escaper: 2.0.2
|
html-escaper: 2.0.2
|
||||||
istanbul-lib-report: 3.0.1
|
istanbul-lib-report: 3.0.1
|
||||||
|
|
||||||
istanbul-reports@3.2.0:
|
|
||||||
dependencies:
|
|
||||||
html-escaper: 2.0.2
|
|
||||||
istanbul-lib-report: 3.0.1
|
|
||||||
|
|
||||||
iterare@1.2.1: {}
|
iterare@1.2.1: {}
|
||||||
|
|
||||||
iterator.prototype@1.1.5:
|
iterator.prototype@1.1.5:
|
||||||
@@ -19001,8 +18900,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
base64-js: 1.5.1
|
base64-js: 1.5.1
|
||||||
|
|
||||||
js-tokens@10.0.0: {}
|
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
|
|
||||||
js-yaml@3.14.2:
|
js-yaml@3.14.2:
|
||||||
@@ -19439,12 +19336,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
magicast@0.5.3:
|
|
||||||
dependencies:
|
|
||||||
'@babel/parser': 7.29.7
|
|
||||||
'@babel/types': 7.29.7
|
|
||||||
source-map-js: 1.2.1
|
|
||||||
|
|
||||||
make-dir@2.1.0:
|
make-dir@2.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
pify: 4.0.1
|
pify: 4.0.1
|
||||||
@@ -21802,25 +21693,7 @@ snapshots:
|
|||||||
tsx: 4.21.0
|
tsx: 4.21.0
|
||||||
yaml: 2.8.3
|
yaml: 2.8.3
|
||||||
|
|
||||||
vite@8.0.5(@types/node@25.5.0)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3):
|
vitest@4.1.6(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(happy-dom@20.8.9)(jsdom@25.0.0)(vite@8.0.5(@types/node@20.19.43)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)):
|
||||||
dependencies:
|
|
||||||
lightningcss: 1.32.0
|
|
||||||
picomatch: 4.0.4
|
|
||||||
postcss: 8.5.14
|
|
||||||
rolldown: 1.0.0-rc.12
|
|
||||||
tinyglobby: 0.2.15
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/node': 25.5.0
|
|
||||||
esbuild: 0.28.0
|
|
||||||
fsevents: 2.3.3
|
|
||||||
jiti: 2.4.2
|
|
||||||
less: 4.2.0
|
|
||||||
sugarss: 5.0.1(postcss@8.5.14)
|
|
||||||
terser: 5.39.0
|
|
||||||
tsx: 4.21.0
|
|
||||||
yaml: 2.8.3
|
|
||||||
|
|
||||||
vitest@4.1.6(@opentelemetry/api@1.9.0)(@types/node@20.19.43)(@vitest/coverage-v8@4.1.6)(happy-dom@20.8.9)(jsdom@25.0.0)(vite@8.0.5(@types/node@20.19.43)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)):
|
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/expect': 4.1.6
|
'@vitest/expect': 4.1.6
|
||||||
'@vitest/mocker': 4.1.6(vite@8.0.5(@types/node@20.19.43)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))
|
'@vitest/mocker': 4.1.6(vite@8.0.5(@types/node@20.19.43)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))
|
||||||
@@ -21845,13 +21718,12 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@opentelemetry/api': 1.9.0
|
'@opentelemetry/api': 1.9.0
|
||||||
'@types/node': 20.19.43
|
'@types/node': 20.19.43
|
||||||
'@vitest/coverage-v8': 4.1.6(vitest@4.1.6)
|
|
||||||
happy-dom: 20.8.9
|
happy-dom: 20.8.9
|
||||||
jsdom: 25.0.0
|
jsdom: 25.0.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- msw
|
- msw
|
||||||
|
|
||||||
vitest@4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(@vitest/coverage-v8@4.1.6)(happy-dom@20.8.9)(jsdom@25.0.0)(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)):
|
vitest@4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(happy-dom@20.8.9)(jsdom@25.0.0)(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/expect': 4.1.6
|
'@vitest/expect': 4.1.6
|
||||||
'@vitest/mocker': 4.1.6(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))
|
'@vitest/mocker': 4.1.6(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))
|
||||||
@@ -21876,43 +21748,11 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@opentelemetry/api': 1.9.0
|
'@opentelemetry/api': 1.9.0
|
||||||
'@types/node': 22.19.1
|
'@types/node': 22.19.1
|
||||||
'@vitest/coverage-v8': 4.1.6(vitest@4.1.6)
|
|
||||||
happy-dom: 20.8.9
|
happy-dom: 20.8.9
|
||||||
jsdom: 25.0.0
|
jsdom: 25.0.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- msw
|
- msw
|
||||||
|
|
||||||
vitest@4.1.6(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(@vitest/coverage-v8@4.1.6)(happy-dom@20.8.9)(jsdom@27.4.0(@noble/hashes@2.0.1))(vite@8.0.5(@types/node@25.5.0)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)):
|
|
||||||
dependencies:
|
|
||||||
'@vitest/expect': 4.1.6
|
|
||||||
'@vitest/mocker': 4.1.6(vite@8.0.5(@types/node@25.5.0)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))
|
|
||||||
'@vitest/pretty-format': 4.1.6
|
|
||||||
'@vitest/runner': 4.1.6
|
|
||||||
'@vitest/snapshot': 4.1.6
|
|
||||||
'@vitest/spy': 4.1.6
|
|
||||||
'@vitest/utils': 4.1.6
|
|
||||||
es-module-lexer: 2.1.0
|
|
||||||
expect-type: 1.3.0
|
|
||||||
magic-string: 0.30.21
|
|
||||||
obug: 2.1.1
|
|
||||||
pathe: 2.0.3
|
|
||||||
picomatch: 4.0.4
|
|
||||||
std-env: 4.1.0
|
|
||||||
tinybench: 2.9.0
|
|
||||||
tinyexec: 1.1.2
|
|
||||||
tinyglobby: 0.2.15
|
|
||||||
tinyrainbow: 3.1.0
|
|
||||||
vite: 8.0.5(@types/node@25.5.0)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)
|
|
||||||
why-is-node-running: 2.3.0
|
|
||||||
optionalDependencies:
|
|
||||||
'@opentelemetry/api': 1.9.0
|
|
||||||
'@types/node': 25.5.0
|
|
||||||
'@vitest/coverage-v8': 4.1.6(vitest@4.1.6)
|
|
||||||
happy-dom: 20.8.9
|
|
||||||
jsdom: 27.4.0(@noble/hashes@2.0.1)
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- msw
|
|
||||||
|
|
||||||
void-elements@3.1.0: {}
|
void-elements@3.1.0: {}
|
||||||
|
|
||||||
vscode-jsonrpc@8.2.0: {}
|
vscode-jsonrpc@8.2.0: {}
|
||||||
|
|||||||
Reference in New Issue
Block a user