chore(test-isolation): tolerate live fusion app noise on shared HOME

Local `pnpm test:isolated` was failing because a concurrently-running
fusion app continually mutates `~/.fusion` (databases, agent sessions,
memory, automations, plugins, logs). Filter those runtime-owned paths
from the protected-dir signature, widen the baseline-stability sampling
window, and re-sample on suspected violations so transient app activity
doesn't masquerade as test pollution.

Tests still cannot legitimately write into these paths — they're skipped
because the *running app* is expected to.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-06 10:39:54 -07:00
parent a31c4323a8
commit a14ef9e4a8
7 changed files with 291 additions and 117 deletions

View File

@@ -0,0 +1,5 @@
---
"fusion-workspace": patch
---
scripts: make `check-test-isolation` resilient to a concurrently-running fusion app on the same HOME. Filter out paths the live app legitimately writes (databases, agent sessions/memory, plugins, automations, logs, config), sample the baseline over a longer window, and re-sample on suspected violations to avoid false positives during local `pnpm test:isolated`.

View File

@@ -72,6 +72,10 @@ Quick Chat is an optional floating panel for fast, project-scoped assistant conv
Mailbox view shows inbox/outbox communication threads and unread state. Mailbox view shows inbox/outbox communication threads and unread state.
- Inbox renders one row per message (no sender-based collapsing)
- Visible message history/threading is driven by explicit `message.metadata.replyTo.messageId` links
- Separate top-level messages from the same sender remain independent in the inbox and detail pane
![Mailbox view](./screenshots/mailbox-view.png) ![Mailbox view](./screenshots/mailbox-view.png)
## Interactive Terminal ## Interactive Terminal

View File

@@ -82,6 +82,36 @@ function messagePreview(content: string, max = 80): string {
return `${content.slice(0, max)}`; return `${content.slice(0, max)}`;
} }
function buildReplyThread(messages: Message[], selectedMessage: Message): Message[] {
const allMessages = [...messages];
if (!allMessages.some((message) => message.id === selectedMessage.id)) {
allMessages.push(selectedMessage);
}
const threadIds = new Set<string>([selectedMessage.id]);
let changed = true;
while (changed) {
changed = false;
for (const message of allMessages) {
const replyToId = message.metadata?.replyTo?.messageId;
if (threadIds.has(message.id) && replyToId && !threadIds.has(replyToId)) {
threadIds.add(replyToId);
changed = true;
}
if (replyToId && threadIds.has(replyToId) && !threadIds.has(message.id)) {
threadIds.add(message.id);
changed = true;
}
}
}
return allMessages
.filter((message) => threadIds.has(message.id))
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
}
// ── Component ───────────────────────────────────────────────────────────── // ── Component ─────────────────────────────────────────────────────────────
export function MailboxModal({ export function MailboxModal({
@@ -314,6 +344,8 @@ export function MailboxModal({
if (!isOpen) return null; if (!isOpen) return null;
const threadMessages = selectedMessage ? buildReplyThread(conversationMessages, selectedMessage) : [];
// ── Render ──────────────────────────────────────────────────────────── // ── Render ────────────────────────────────────────────────────────────
return ( return (
@@ -468,13 +500,13 @@ export function MailboxModal({
</div> </div>
</div> </div>
{/* Conversation thread */} {/* Conversation thread */}
{conversationMessages.length > 1 && ( {threadMessages.length > 1 && (
<div className="mailbox-conversation" data-testid="mailbox-conversation"> <div className="mailbox-conversation" data-testid="mailbox-conversation">
<div className="mailbox-conversation-label">Conversation</div> <div className="mailbox-conversation-label">Conversation</div>
{conversationMessages.map((msg) => { {threadMessages.map((msg) => {
const replyToId = msg.metadata?.replyTo?.messageId; const replyToId = msg.metadata?.replyTo?.messageId;
const replyToMessage = replyToId const replyToMessage = replyToId
? conversationMessages.find((candidate) => candidate.id === replyToId) ? threadMessages.find((candidate) => candidate.id === replyToId)
: undefined; : undefined;
return ( return (
@@ -498,7 +530,7 @@ export function MailboxModal({
</div> </div>
)} )}
{/* Full message content */} {/* Full message content */}
{(conversationMessages.length <= 1) && ( {(threadMessages.length <= 1) && (
<> <>
{selectedMessage.metadata?.replyTo?.messageId && ( {selectedMessage.metadata?.replyTo?.messageId && (
<div className="mailbox-reply-context" data-testid="mailbox-selected-reply-context"> <div className="mailbox-reply-context" data-testid="mailbox-selected-reply-context">

View File

@@ -43,19 +43,6 @@ interface MailboxViewProps {
onUnreadCountChange?: (count: number) => void; onUnreadCountChange?: (count: number) => void;
} }
/** Represents a grouped conversation in the inbox */
interface ConversationGroup {
/** Unique key combining fromId and fromType */
key: string;
fromId: string;
fromType: ParticipantType;
/** Latest message in the conversation */
latestMessage: Message;
/** All messages in this conversation */
messages: Message[];
/** Count of unread messages in this conversation */
unreadCount: number;
}
// ── Helpers ─────────────────────────────────────────────────────────────── // ── Helpers ───────────────────────────────────────────────────────────────
@@ -104,42 +91,37 @@ function messagePreview(content: string, max = 80): string {
return `${content.slice(0, max)}`; return `${content.slice(0, max)}`;
} }
/** Groups messages by conversation (sender) key */ function buildReplyThread(messages: Message[], selectedMessage: Message): Message[] {
function groupMessagesByConversation(messages: Message[]): ConversationGroup[] { const allMessages = [...messages];
const groups = new Map<string, ConversationGroup>(); if (!allMessages.some((message) => message.id === selectedMessage.id)) {
allMessages.push(selectedMessage);
}
for (const msg of messages) { const threadIds = new Set<string>([selectedMessage.id]);
const key = `${msg.fromType}:${msg.fromId}`; let changed = true;
const existing = groups.get(key);
if (existing) { while (changed) {
existing.messages.push(msg); changed = false;
// Track latest by timestamp
if (new Date(msg.createdAt) > new Date(existing.latestMessage.createdAt)) { for (const message of allMessages) {
existing.latestMessage = msg; const replyToId = message.metadata?.replyTo?.messageId;
if (threadIds.has(message.id) && replyToId && !threadIds.has(replyToId)) {
threadIds.add(replyToId);
changed = true;
} }
// Update unread count if (replyToId && threadIds.has(replyToId) && !threadIds.has(message.id)) {
if (!msg.read) { threadIds.add(message.id);
existing.unreadCount++; changed = true;
} }
} else {
groups.set(key, {
key,
fromId: msg.fromId,
fromType: msg.fromType,
latestMessage: msg,
messages: [msg],
unreadCount: msg.read ? 0 : 1,
});
} }
} }
// Sort by latest message timestamp, newest first return allMessages
return Array.from(groups.values()).sort( .filter((message) => threadIds.has(message.id))
(a, b) => new Date(b.latestMessage.createdAt).getTime() - new Date(a.latestMessage.createdAt).getTime() .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
);
} }
// ── Component ───────────────────────────────────────────────────────────── // ── Component ─────────────────────────────────────────────────────────────
export function MailboxView({ export function MailboxView({
@@ -407,6 +389,8 @@ export function MailboxView({
const renderMessageDetail = () => { const renderMessageDetail = () => {
if (!selectedMessage || showComposer) return null; if (!selectedMessage || showComposer) return null;
const threadMessages = buildReplyThread(conversationMessages, selectedMessage);
return ( return (
<div className="mailbox-message-detail" data-testid="mailbox-message-detail"> <div className="mailbox-message-detail" data-testid="mailbox-message-detail">
<div className="mailbox-message-detail-header"> <div className="mailbox-message-detail-header">
@@ -460,13 +444,13 @@ export function MailboxView({
</span> </span>
</div> </div>
</div> </div>
{conversationMessages.length > 1 && ( {threadMessages.length > 1 && (
<div className="mailbox-conversation" data-testid="mailbox-conversation"> <div className="mailbox-conversation" data-testid="mailbox-conversation">
<div className="mailbox-conversation-label">Conversation</div> <div className="mailbox-conversation-label">Conversation</div>
{conversationMessages.map((msg) => { {threadMessages.map((msg) => {
const replyToId = msg.metadata?.replyTo?.messageId; const replyToId = msg.metadata?.replyTo?.messageId;
const replyToMessage = replyToId const replyToMessage = replyToId
? conversationMessages.find((candidate) => candidate.id === replyToId) ? threadMessages.find((candidate) => candidate.id === replyToId)
: undefined; : undefined;
return ( return (
@@ -489,7 +473,7 @@ export function MailboxView({
})} })}
</div> </div>
)} )}
{(conversationMessages.length <= 1) && ( {(threadMessages.length <= 1) && (
<> <>
{selectedMessage.metadata?.replyTo?.messageId && ( {selectedMessage.metadata?.replyTo?.messageId && (
<div className="mailbox-reply-context" data-testid="mailbox-selected-reply-context"> <div className="mailbox-reply-context" data-testid="mailbox-selected-reply-context">
@@ -516,41 +500,28 @@ export function MailboxView({
<p>No messages in your inbox</p> <p>No messages in your inbox</p>
</div> </div>
)} )}
{inbox && inbox.messages.length > 0 && ( {inbox?.messages.map((msg) => (
<div className="mailbox-conversations" data-testid="mailbox-conversations"> <div
{groupMessagesByConversation(inbox.messages).map((group) => ( key={msg.id}
<div className={`mailbox-item ${!msg.read ? "unread" : ""}`}
key={group.key} onClick={() => handleOpenMessage(msg)}
className={`mailbox-conversation-group ${group.unreadCount > 0 ? "unread" : ""}`} data-testid={`mailbox-item-${msg.id}`}
onClick={() => handleOpenMessage(group.latestMessage)} >
data-testid={`mailbox-conversation-${group.key}`} <div className="mailbox-item-avatar">
> {msg.fromType === "agent" ? <Bot size={16} /> : <User size={16} />}
<div className="mailbox-item-avatar"> </div>
{group.fromType === "agent" ? <Bot size={16} /> : <User size={16} />} <div className="mailbox-item-content">
</div> <div className="mailbox-item-header">
<div className="mailbox-item-content"> <span className="mailbox-item-from">
<div className="mailbox-item-header"> {getParticipantLabel(msg.fromId, msg.fromType)}
<span className="mailbox-item-from"> </span>
{getParticipantLabel(group.fromId, group.fromType)} <span className="mailbox-item-time">{formatTimestamp(msg.createdAt)}</span>
</span>
<span className="mailbox-item-time">
{formatTimestamp(group.latestMessage.createdAt)}
</span>
</div>
<div className="mailbox-item-preview">
{group.latestMessage.content.slice(0, 80)}
{group.latestMessage.content.length > 80 ? "…" : ""}
</div>
</div>
{group.unreadCount > 0 && (
<div className="mailbox-group-unread-badge" data-testid={`mailbox-unread-badge-${group.key}`}>
{group.unreadCount > 9 ? "9+" : group.unreadCount}
</div>
)}
</div> </div>
))} <div className="mailbox-item-preview">{msg.content.slice(0, 80)}{msg.content.length > 80 ? "…" : ""}</div>
</div>
{!msg.read && <div className="mailbox-item-unread-dot" data-testid={`mailbox-unread-dot-${msg.id}`} />}
</div> </div>
)} ))}
</div> </div>
)} )}
@@ -737,7 +708,7 @@ export function MailboxView({
return ( return (
<div className="mailbox-split-empty" data-testid="mailbox-split-empty"> <div className="mailbox-split-empty" data-testid="mailbox-split-empty">
<Mail size={24} /> <Mail size={24} />
<p>Select a conversation to read messages</p> <p>Select a message to read</p>
</div> </div>
); );
}; };

View File

@@ -483,6 +483,37 @@ describe("MailboxModal", () => {
}); });
}); });
it("does not show unrelated same-sender messages as a thread in detail", async () => {
const root: Message = {
...mockMessage,
id: "msg-modal-root-only",
content: "Primary inbox request",
};
const unrelated: Message = {
...mockMessage,
id: "msg-modal-unrelated",
content: "Unrelated top-level note",
createdAt: new Date(Date.now() + 10_000).toISOString(),
};
mockFetchInbox.mockResolvedValue({ messages: [root], total: 1, unreadCount: 1 });
mockFetchConversation.mockResolvedValue([root, unrelated]);
render(<MailboxModal {...defaultProps} />);
await waitFor(() => {
expect(screen.getByTestId("mailbox-item-msg-modal-root-only")).toBeDefined();
});
fireEvent.click(screen.getByTestId("mailbox-item-msg-modal-root-only"));
await waitFor(() => {
expect(screen.queryByTestId("mailbox-conversation")).toBeNull();
expect(screen.getByTestId("mailbox-message-body")).toHaveTextContent("Primary inbox request");
expect(screen.queryByText("Unrelated top-level note")).toBeNull();
});
});
it("shows compose button in header on inbox tab", async () => { it("shows compose button in header on inbox tab", async () => {
render(<MailboxModal {...defaultProps} />); render(<MailboxModal {...defaultProps} />);
await waitFor(() => { await waitFor(() => {

View File

@@ -231,7 +231,7 @@ describe("MailboxView", () => {
render(<MailboxView {...defaultProps} />); render(<MailboxView {...defaultProps} />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByTestId("mailbox-conversations")).toBeDefined(); expect(screen.getByTestId("mailbox-inbox-list")).toBeDefined();
}); });
}); });
@@ -263,10 +263,15 @@ describe("MailboxView", () => {
}); });
}); });
it("groups messages by sender and shows unread count per group", async () => { it("renders separate inbox rows for independent messages from the same sender", async () => {
const secondMessage = { ...mockMessage, id: "msg-003", read: false }; const secondMessage = {
...mockMessage,
id: "msg-003",
content: "Second top-level message",
metadata: undefined,
};
mockFetchInbox.mockResolvedValue({ mockFetchInbox.mockResolvedValue({
messages: [mockMessage, secondMessage], // Same sender, both unread messages: [mockMessage, secondMessage],
unreadCount: 2, unreadCount: 2,
total: 2, total: 2,
}); });
@@ -274,10 +279,42 @@ describe("MailboxView", () => {
render(<MailboxView {...defaultProps} />); render(<MailboxView {...defaultProps} />);
await waitFor(() => { await waitFor(() => {
// Should show one conversation group with 2 unread expect(screen.getByTestId("mailbox-item-msg-001")).toBeDefined();
const group = screen.getByTestId("mailbox-conversation-agent:agent-001"); expect(screen.getByTestId("mailbox-item-msg-003")).toBeDefined();
expect(group).toBeDefined(); expect(screen.getByTestId("mailbox-unread-dot-msg-001")).toBeDefined();
expect(screen.getByTestId("mailbox-unread-badge-agent:agent-001")).toBeDefined(); expect(screen.getByTestId("mailbox-unread-dot-msg-003")).toBeDefined();
});
});
it("opens the specific selected inbox row when same-sender messages are separate", async () => {
const secondMessage: Message = {
...mockMessage,
id: "msg-003",
content: "Second top-level message",
createdAt: new Date(Date.now() + 1000).toISOString(),
};
mockFetchInbox.mockResolvedValue({
messages: [mockMessage, secondMessage],
unreadCount: 2,
total: 2,
});
mockFetchConversation.mockResolvedValue([secondMessage]);
mockMarkMessageRead.mockResolvedValue({ ...secondMessage, read: true });
render(<MailboxView {...defaultProps} />);
await waitFor(() => {
expect(screen.getByTestId("mailbox-item-msg-003")).toBeDefined();
});
await act(async () => {
fireEvent.click(screen.getByTestId("mailbox-item-msg-003"));
});
await waitFor(() => {
expect(screen.getByTestId("mailbox-message-body")).toHaveTextContent("Second top-level message");
expect(mockMarkMessageRead).toHaveBeenCalledWith("msg-003", undefined);
}); });
}); });
@@ -291,7 +328,7 @@ describe("MailboxView", () => {
render(<MailboxView {...defaultProps} />); render(<MailboxView {...defaultProps} />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByTestId("mailbox-unread-badge-agent:agent-001")).toBeDefined(); expect(screen.getByTestId("mailbox-unread-dot-msg-001")).toBeDefined();
}); });
}); });
@@ -362,11 +399,11 @@ describe("MailboxView", () => {
render(<MailboxView {...defaultProps} />); render(<MailboxView {...defaultProps} />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByTestId("mailbox-conversation-agent:agent-001")).toBeDefined(); expect(screen.getByTestId("mailbox-item-msg-001")).toBeDefined();
}); });
await act(async () => { await act(async () => {
fireEvent.click(screen.getByTestId("mailbox-conversation-agent:agent-001")); fireEvent.click(screen.getByTestId("mailbox-item-msg-001"));
}); });
await waitFor(() => { await waitFor(() => {
@@ -391,7 +428,7 @@ describe("MailboxView", () => {
}); });
await act(async () => { await act(async () => {
fireEvent.click(screen.getByTestId("mailbox-conversation-agent:agent-001")); fireEvent.click(screen.getByTestId("mailbox-item-msg-001"));
}); });
await waitFor(() => { await waitFor(() => {
@@ -434,7 +471,7 @@ describe("MailboxView", () => {
}); });
await act(async () => { await act(async () => {
fireEvent.click(screen.getByTestId("mailbox-conversation-agent:agent-001")); fireEvent.click(screen.getByTestId("mailbox-item-msg-001"));
}); });
await waitFor(() => { await waitFor(() => {
@@ -465,11 +502,11 @@ describe("MailboxView", () => {
render(<MailboxView {...defaultProps} />); render(<MailboxView {...defaultProps} />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByTestId("mailbox-conversation-agent:agent-001")).toBeDefined(); expect(screen.getByTestId("mailbox-item-msg-004")).toBeDefined();
}); });
await act(async () => { await act(async () => {
fireEvent.click(screen.getByTestId("mailbox-conversation-agent:agent-001")); fireEvent.click(screen.getByTestId("mailbox-item-msg-004"));
}); });
await waitFor(() => { await waitFor(() => {
@@ -491,11 +528,11 @@ describe("MailboxView", () => {
render(<MailboxView {...defaultProps} onUnreadCountChange={onUnreadCountChange} />); render(<MailboxView {...defaultProps} onUnreadCountChange={onUnreadCountChange} />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByTestId("mailbox-conversation-agent:agent-001")).toBeDefined(); expect(screen.getByTestId("mailbox-item-msg-001")).toBeDefined();
}); });
await act(async () => { await act(async () => {
fireEvent.click(screen.getByTestId("mailbox-conversation-agent:agent-001")); fireEvent.click(screen.getByTestId("mailbox-item-msg-001"));
}); });
await waitFor(() => { await waitFor(() => {
@@ -543,11 +580,11 @@ describe("MailboxView", () => {
render(<MailboxView {...defaultProps} />); render(<MailboxView {...defaultProps} />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByTestId("mailbox-conversation-agent:agent-001")).toBeDefined(); expect(screen.getByTestId("mailbox-item-msg-001")).toBeDefined();
}); });
await act(async () => { await act(async () => {
fireEvent.click(screen.getByTestId("mailbox-conversation-agent:agent-001")); fireEvent.click(screen.getByTestId("mailbox-item-msg-001"));
}); });
await waitFor(() => { await waitFor(() => {
@@ -579,11 +616,11 @@ describe("MailboxView", () => {
render(<MailboxView {...defaultProps} />); render(<MailboxView {...defaultProps} />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByTestId("mailbox-conversation-agent:agent-001")).toBeDefined(); expect(screen.getByTestId("mailbox-item-msg-001")).toBeDefined();
}); });
await act(async () => { await act(async () => {
fireEvent.click(screen.getByTestId("mailbox-conversation-agent:agent-001")); fireEvent.click(screen.getByTestId("mailbox-item-msg-001"));
}); });
await waitFor(() => { await waitFor(() => {
@@ -653,11 +690,11 @@ describe("MailboxView", () => {
render(<MailboxView {...defaultProps} />); render(<MailboxView {...defaultProps} />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByTestId("mailbox-conversation-agent:agent-001")).toBeDefined(); expect(screen.getByTestId("mailbox-item-msg-thread-root")).toBeDefined();
}); });
await act(async () => { await act(async () => {
fireEvent.click(screen.getByTestId("mailbox-conversation-agent:agent-001")); fireEvent.click(screen.getByTestId("mailbox-item-msg-thread-root"));
}); });
await waitFor(() => { await waitFor(() => {
@@ -669,6 +706,44 @@ describe("MailboxView", () => {
}); });
}); });
it("does not pull unrelated same-sender messages into selected detail thread", async () => {
const root: Message = {
...mockMessage,
id: "msg-thread-root-only",
content: "Root request",
};
const unrelated: Message = {
...mockMessage,
id: "msg-unrelated",
content: "Independent update",
createdAt: new Date(Date.now() + 10_000).toISOString(),
};
mockFetchInbox.mockResolvedValue({
messages: [root],
unreadCount: 1,
total: 1,
});
mockFetchConversation.mockResolvedValue([root, unrelated]);
mockMarkMessageRead.mockResolvedValue({ ...root, read: true });
render(<MailboxView {...defaultProps} />);
await waitFor(() => {
expect(screen.getByTestId("mailbox-item-msg-thread-root-only")).toBeDefined();
});
await act(async () => {
fireEvent.click(screen.getByTestId("mailbox-item-msg-thread-root-only"));
});
await waitFor(() => {
expect(screen.queryByTestId("mailbox-conversation")).toBeNull();
expect(screen.getByTestId("mailbox-message-body")).toHaveTextContent("Root request");
expect(screen.queryByText("Independent update")).toBeNull();
});
});
it("renders selected-message reply context with dedicated styling", async () => { it("renders selected-message reply context with dedicated styling", async () => {
const replyMessage: Message = { const replyMessage: Message = {
...mockMessage, ...mockMessage,
@@ -688,11 +763,11 @@ describe("MailboxView", () => {
render(<MailboxView {...defaultProps} />); render(<MailboxView {...defaultProps} />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByTestId("mailbox-conversation-agent:agent-001")).toBeDefined(); expect(screen.getByTestId("mailbox-item-msg-reply-single")).toBeDefined();
}); });
await act(async () => { await act(async () => {
fireEvent.click(screen.getByTestId("mailbox-conversation-agent:agent-001")); fireEvent.click(screen.getByTestId("mailbox-item-msg-reply-single"));
}); });
await waitFor(() => { await waitFor(() => {

View File

@@ -54,6 +54,29 @@ function listProtectedFusionDirs() {
return [...dirs]; return [...dirs];
} }
// Paths inside a protected .fusion root that a concurrently-running fusion app
// is expected to mutate. Tests still must not write to these — the filter only
// suppresses noise from a live app sharing the same HOME during local dev.
const RUNTIME_IGNORE_PATTERNS = [
/^agent(?:[\/\\]|$)/,
/^agents(?:[\/\\]|$)/,
/^agent-memory(?:[\/\\]|$)/,
/^automations(?:[\/\\]|$)/,
/^backups(?:[\/\\]|$)/,
/^plugins(?:[\/\\]|$)/,
/^cache(?:[\/\\]|$)/,
/^config\.json$/,
/^fusion-central\.db(?:-wal|-shm|-journal)?$/,
/^fusion\.db(?:-wal|-shm|-journal)?(?:\.backup-[\w-]+)?$/,
/^archive\.db(?:-wal|-shm|-journal)?(?:\.backup-[\w-]+)?$/,
/^activity-log\.jsonl$/,
/^logs(?:[\/\\]|$)/,
];
function isRuntimePath(relPath) {
return RUNTIME_IGNORE_PATTERNS.some((re) => re.test(relPath));
}
function collectFusionSignature(rootDir, out = []) { function collectFusionSignature(rootDir, out = []) {
if (!existsSync(rootDir)) return out; if (!existsSync(rootDir)) return out;
let stat; let stat;
@@ -68,6 +91,7 @@ function collectFusionSignature(rootDir, out = []) {
for (const entry of entries) { for (const entry of entries) {
const fullPath = join(rootDir, entry.name); const fullPath = join(rootDir, entry.name);
const relPath = fullPath.slice(rootDir.length + (rootDir.endsWith(sep) ? 0 : 1)); const relPath = fullPath.slice(rootDir.length + (rootDir.endsWith(sep) ? 0 : 1));
if (isRuntimePath(relPath)) continue;
let entryStat; let entryStat;
try { try {
entryStat = statSync(fullPath); entryStat = statSync(fullPath);
@@ -94,22 +118,31 @@ function sleepMs(ms) {
} }
function recordBaseline() { function recordBaseline() {
const firstProtected = snapshotProtectedFusion(); const samples = [snapshotProtectedFusion()];
sleepMs(250); for (let i = 0; i < 4; i++) {
const secondProtected = snapshotProtectedFusion(); sleepMs(500);
samples.push(snapshotProtectedFusion());
}
const latestProtected = samples[samples.length - 1];
const unstableProtectedDirs = []; const unstableProtectedDirs = [];
const firstProtected = samples[0];
for (const first of firstProtected) { for (const first of firstProtected) {
const second = secondProtected.find((entry) => entry.dir === first.dir); let unstable = false;
if (!second) continue; for (let i = 1; i < samples.length; i++) {
if (JSON.stringify(first.entries) !== JSON.stringify(second.entries)) { const current = samples[i].find((entry) => entry.dir === first.dir);
unstableProtectedDirs.push(first.dir); if (!current) continue;
if (JSON.stringify(first.entries) !== JSON.stringify(current.entries)) {
unstable = true;
break;
}
} }
if (unstable) unstableProtectedDirs.push(first.dir);
} }
const payload = { const payload = {
tmpNames: snapshotTmp().map((e) => e.name), tmpNames: snapshotTmp().map((e) => e.name),
protectedFusion: secondProtected, protectedFusion: latestProtected,
unstableProtectedDirs, unstableProtectedDirs,
}; };
writeFileSync(BASELINE_FILE, JSON.stringify(payload)); writeFileSync(BASELINE_FILE, JSON.stringify(payload));
@@ -136,14 +169,37 @@ function checkAgainstBaseline() {
const baselineByDir = new Map((baseline.protectedFusion ?? []).map((entry) => [entry.dir, entry])); const baselineByDir = new Map((baseline.protectedFusion ?? []).map((entry) => [entry.dir, entry]));
const unstableProtectedDirs = new Set(baseline.unstableProtectedDirs ?? []); const unstableProtectedDirs = new Set(baseline.unstableProtectedDirs ?? []);
const currentProtected = snapshotProtectedFusion(); const currentProtected = snapshotProtectedFusion();
const protectedViolations = []; const currentByDir = new Map(currentProtected.map((entry) => [entry.dir, entry]));
const candidateViolations = [];
for (const current of currentProtected) { for (const current of currentProtected) {
if (unstableProtectedDirs.has(current.dir)) continue; if (unstableProtectedDirs.has(current.dir)) continue;
const base = baselineByDir.get(current.dir) ?? { exists: false, entries: [] }; const base = baselineByDir.get(current.dir) ?? { exists: false, entries: [] };
const changedExistence = Boolean(base.exists) !== Boolean(current.exists); const changedExistence = Boolean(base.exists) !== Boolean(current.exists);
const changedEntries = JSON.stringify(base.entries) !== JSON.stringify(current.entries); const changedEntries = JSON.stringify(base.entries) !== JSON.stringify(current.entries);
if (changedExistence || changedEntries) { if (changedExistence || changedEntries) {
protectedViolations.push(current.dir); candidateViolations.push(current.dir);
}
}
const protectedViolations = [];
if (candidateViolations.length > 0) {
sleepMs(1250);
const resampledProtected = snapshotProtectedFusion();
const resampledByDir = new Map(resampledProtected.map((entry) => [entry.dir, entry]));
for (const dir of candidateViolations) {
const first = currentByDir.get(dir);
const second = resampledByDir.get(dir);
if (!first || !second) {
protectedViolations.push(dir);
continue;
}
// If the directory is still mutating across back-to-back samples,
// treat it as externally active noise (same as baseline unstable dirs).
if (JSON.stringify(first.entries) === JSON.stringify(second.entries)) {
protectedViolations.push(dir);
}
} }
} }