import { assert, it, describe } from "@effect/vitest";import * as NodeServices from "@effect/platform-node/NodeServices";import * as Deferred from "effect/Deferred";import * as Duration from "effect/Duration";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Path from "effect/Path";import * as Scope from "effect/Scope";import * as Stream from "effect/Stream";import type { VcsStatusLocalResult, VcsStatusRemoteResult, VcsStatusResult, VcsStatusStreamEvent,} from "@t3tools/contracts"; import * as VcsStatusBroadcaster from "./VcsStatusBroadcaster.ts";import * as GitWorkflowService from "../git/GitWorkflowService.ts"; const baseLocalStatus: VcsStatusLocalResult = { isRepo: true, sourceControlProvider: { kind: "github", name: "GitHub", baseUrl: "https://github.com", }, hasPrimaryRemote: true, isDefaultRef: false, refName: "feature/status-broadcast", hasWorkingTreeChanges: false, workingTree: { files: [], insertions: 0, deletions: 0 },}; const baseRemoteStatus: VcsStatusRemoteResult = { hasUpstream: true, aheadCount: 0, behindCount: 0, pr: null,}; const baseStatus: VcsStatusResult = { ...baseLocalStatus, ...baseRemoteStatus,}; function makeTestLayer(state: { currentLocalStatus: VcsStatusLocalResult; currentRemoteStatus: VcsStatusRemoteResult | null; localStatusCalls: number; remoteStatusCalls: number; localInvalidationCalls: number; remoteInvalidationCalls: number;}) { return VcsStatusBroadcaster.layer.pipe( Layer.provideMerge(NodeServices.layer), Layer.provide( Layer.mock(GitWorkflowService.GitWorkflowService)({ localStatus: () => Effect.sync(() => { state.localStatusCalls += 1; return state.currentLocalStatus; }), remoteStatus: () => Effect.sync(() => { state.remoteStatusCalls += 1; return state.currentRemoteStatus; }), invalidateLocalStatus: () => Effect.sync(() => { state.localInvalidationCalls += 1; }), invalidateRemoteStatus: () => Effect.sync(() => { state.remoteInvalidationCalls += 1; }), }), ), );} describe("VcsStatusBroadcaster", () => { it.effect("reuses the cached VCS status across repeated reads", () => { const state = { currentLocalStatus: baseLocalStatus, currentRemoteStatus: baseRemoteStatus, localStatusCalls: 0, remoteStatusCalls: 0, localInvalidationCalls: 0, remoteInvalidationCalls: 0, }; return Effect.gen(function* () { const broadcaster = yield* VcsStatusBroadcaster.VcsStatusBroadcaster; const first = yield* broadcaster.getStatus({ cwd: "/repo" }); const second = yield* broadcaster.getStatus({ cwd: "/repo" }); assert.deepStrictEqual(first, baseStatus); assert.deepStrictEqual(second, baseStatus); assert.equal(state.localStatusCalls, 1); assert.equal(state.remoteStatusCalls, 1); assert.equal(state.localInvalidationCalls, 0); assert.equal(state.remoteInvalidationCalls, 0); }).pipe(Effect.provide(makeTestLayer(state))); }); it.effect("refreshes the cached snapshot after explicit invalidation", () => { const state = { currentLocalStatus: baseLocalStatus, currentRemoteStatus: baseRemoteStatus, localStatusCalls: 0, remoteStatusCalls: 0, localInvalidationCalls: 0, remoteInvalidationCalls: 0, }; return Effect.gen(function* () { const broadcaster = yield* VcsStatusBroadcaster.VcsStatusBroadcaster; const initial = yield* broadcaster.getStatus({ cwd: "/repo" }); state.currentLocalStatus = { ...baseLocalStatus, refName: "feature/updated-status", }; state.currentRemoteStatus = { ...baseRemoteStatus, aheadCount: 2, }; const refreshed = yield* broadcaster.refreshStatus("/repo"); const cached = yield* broadcaster.getStatus({ cwd: "/repo" }); assert.deepStrictEqual(initial, baseStatus); assert.deepStrictEqual(refreshed, { ...state.currentLocalStatus, ...state.currentRemoteStatus, }); assert.deepStrictEqual(cached, { ...state.currentLocalStatus, ...state.currentRemoteStatus, }); assert.equal(state.localStatusCalls, 2); assert.equal(state.remoteStatusCalls, 2); assert.equal(state.localInvalidationCalls, 1); assert.equal(state.remoteInvalidationCalls, 1); }).pipe(Effect.provide(makeTestLayer(state))); }); it.effect("refreshes only the cached local snapshot when requested", () => { const state = { currentLocalStatus: baseLocalStatus, currentRemoteStatus: baseRemoteStatus, localStatusCalls: 0, remoteStatusCalls: 0, localInvalidationCalls: 0, remoteInvalidationCalls: 0, }; return Effect.gen(function* () { const broadcaster = yield* VcsStatusBroadcaster.VcsStatusBroadcaster; const initial = yield* broadcaster.getStatus({ cwd: "/repo" }); state.currentLocalStatus = { ...baseLocalStatus, refName: "feature/local-only-refresh", hasWorkingTreeChanges: true, }; const refreshedLocal = yield* broadcaster.refreshLocalStatus("/repo"); const cached = yield* broadcaster.getStatus({ cwd: "/repo" }); assert.deepStrictEqual(initial, baseStatus); assert.deepStrictEqual(refreshedLocal, state.currentLocalStatus); assert.deepStrictEqual(cached, { ...state.currentLocalStatus, ...baseRemoteStatus, }); assert.equal(state.localStatusCalls, 2); assert.equal(state.remoteStatusCalls, 1); assert.equal(state.localInvalidationCalls, 1); assert.equal(state.remoteInvalidationCalls, 0); }).pipe(Effect.provide(makeTestLayer(state))); }); it.effect("normalizes symlinked CWDs before cache lookup and workflow calls", () => { const seenCwds: string[] = []; const state = { currentLocalStatus: baseLocalStatus, currentRemoteStatus: baseRemoteStatus, localStatusCalls: 0, remoteStatusCalls: 0, localInvalidationCalls: 0, remoteInvalidationCalls: 0, }; const testLayer = VcsStatusBroadcaster.layer.pipe( Layer.provideMerge(NodeServices.layer), Layer.provide( Layer.mock(GitWorkflowService.GitWorkflowService)({ localStatus: (input) => Effect.sync(() => { seenCwds.push(input.cwd); state.localStatusCalls += 1; return state.currentLocalStatus; }), remoteStatus: (input) => Effect.sync(() => { seenCwds.push(input.cwd); state.remoteStatusCalls += 1; return state.currentRemoteStatus; }), invalidateLocalStatus: () => Effect.sync(() => { state.localInvalidationCalls += 1; }), invalidateRemoteStatus: () => Effect.sync(() => { state.remoteInvalidationCalls += 1; }), } satisfies Partial<GitWorkflowService.GitWorkflowServiceShape>), ), ); return Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const realDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-vcs-status-real-", }); const linkParent = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-vcs-status-link-", }); const linkDir = path.join(linkParent, "repo-link"); yield* fileSystem.symlink(realDir, linkDir); const realPath = yield* fileSystem.realPath(realDir); const broadcaster = yield* VcsStatusBroadcaster.VcsStatusBroadcaster; yield* broadcaster.getStatus({ cwd: linkDir }); yield* broadcaster.getStatus({ cwd: realDir }); assert.deepStrictEqual(seenCwds, [realPath, realPath]); assert.equal(state.localStatusCalls, 1); assert.equal(state.remoteStatusCalls, 1); }).pipe(Effect.provide(testLayer)); }); it.effect("streams a local snapshot first and remote updates later", () => { const state = { currentLocalStatus: baseLocalStatus, currentRemoteStatus: baseRemoteStatus, localStatusCalls: 0, remoteStatusCalls: 0, localInvalidationCalls: 0, remoteInvalidationCalls: 0, }; return Effect.gen(function* () { const broadcaster = yield* VcsStatusBroadcaster.VcsStatusBroadcaster; const snapshotDeferred = yield* Deferred.make<VcsStatusStreamEvent>(); const remoteUpdatedDeferred = yield* Deferred.make<VcsStatusStreamEvent>(); yield* Stream.runForEach(broadcaster.streamStatus({ cwd: "/repo" }), (event) => { if (event._tag === "snapshot") { return Deferred.succeed(snapshotDeferred, event).pipe(Effect.ignore); } if (event._tag === "remoteUpdated") { return Deferred.succeed(remoteUpdatedDeferred, event).pipe(Effect.ignore); } return Effect.void; }).pipe(Effect.forkScoped); const snapshot = yield* Deferred.await(snapshotDeferred); yield* broadcaster.refreshStatus("/repo"); const remoteUpdated = yield* Deferred.await(remoteUpdatedDeferred); assert.deepStrictEqual(snapshot, { _tag: "snapshot", local: baseLocalStatus, remote: null, } satisfies VcsStatusStreamEvent); assert.deepStrictEqual(remoteUpdated, { _tag: "remoteUpdated", remote: baseRemoteStatus, } satisfies VcsStatusStreamEvent); }).pipe(Effect.provide(makeTestLayer(state))); }); it.effect("does not start automatic remote refreshes when disabled", () => { const state = { currentLocalStatus: baseLocalStatus, currentRemoteStatus: baseRemoteStatus, localStatusCalls: 0, remoteStatusCalls: 0, localInvalidationCalls: 0, remoteInvalidationCalls: 0, }; return Effect.gen(function* () { const broadcaster = yield* VcsStatusBroadcaster.VcsStatusBroadcaster; const snapshot = yield* Stream.runHead( broadcaster.streamStatus( { cwd: "/repo" }, { automaticRemoteRefreshInterval: Effect.succeed(Duration.zero) }, ), ); assert.isTrue(Option.isSome(snapshot)); assert.equal(state.remoteStatusCalls, 0); assert.equal(state.remoteInvalidationCalls, 0); }).pipe(Effect.provide(makeTestLayer(state))); }); it("backs off remote refresh failures exponentially and honors larger configured intervals", () => { assert.equal( Duration.toMillis(VcsStatusBroadcaster.remoteRefreshFailureDelay(1, Duration.seconds(1))), 30_000, ); assert.equal( Duration.toMillis(VcsStatusBroadcaster.remoteRefreshFailureDelay(2, Duration.seconds(1))), 60_000, ); assert.equal( Duration.toMillis(VcsStatusBroadcaster.remoteRefreshFailureDelay(3, Duration.seconds(1))), 120_000, ); assert.equal( Duration.toMillis(VcsStatusBroadcaster.remoteRefreshFailureDelay(1, Duration.minutes(5))), 300_000, ); assert.equal( Duration.toMillis(VcsStatusBroadcaster.remoteRefreshFailureDelay(20, Duration.seconds(1))), 900_000, ); }); it.effect("stops the remote poller after the last stream subscriber disconnects", () => { const state = { currentLocalStatus: baseLocalStatus, currentRemoteStatus: baseRemoteStatus, localStatusCalls: 0, remoteStatusCalls: 0, localInvalidationCalls: 0, remoteInvalidationCalls: 0, }; let remoteInterruptedDeferred: Deferred.Deferred<void, never> | null = null; let remoteStartedDeferred: Deferred.Deferred<void, never> | null = null; const testLayer = VcsStatusBroadcaster.layer.pipe( Layer.provideMerge(NodeServices.layer), Layer.provide( Layer.mock(GitWorkflowService.GitWorkflowService)({ localStatus: () => Effect.sync(() => { state.localStatusCalls += 1; return state.currentLocalStatus; }), remoteStatus: () => Effect.sync(() => { state.remoteStatusCalls += 1; }).pipe( Effect.andThen( remoteStartedDeferred ? Deferred.succeed(remoteStartedDeferred, undefined).pipe(Effect.ignore) : Effect.void, ), Effect.andThen(Effect.never as Effect.Effect<VcsStatusRemoteResult | null, never>), Effect.onInterrupt(() => remoteInterruptedDeferred ? Deferred.succeed(remoteInterruptedDeferred, undefined).pipe(Effect.ignore) : Effect.void, ), ), invalidateLocalStatus: () => Effect.sync(() => { state.localInvalidationCalls += 1; }), invalidateRemoteStatus: () => Effect.sync(() => { state.remoteInvalidationCalls += 1; }), } satisfies Partial<GitWorkflowService.GitWorkflowServiceShape>), ), ); return Effect.gen(function* () { const remoteInterrupted = yield* Deferred.make<void>(); const remoteStarted = yield* Deferred.make<void>(); remoteInterruptedDeferred = remoteInterrupted; remoteStartedDeferred = remoteStarted; const broadcaster = yield* VcsStatusBroadcaster.VcsStatusBroadcaster; const firstSnapshot = yield* Deferred.make<VcsStatusStreamEvent>(); const secondSnapshot = yield* Deferred.make<VcsStatusStreamEvent>(); const firstScope = yield* Scope.make(); const secondScope = yield* Scope.make(); yield* Stream.runForEach(broadcaster.streamStatus({ cwd: "/repo" }), (event) => event._tag === "snapshot" ? Deferred.succeed(firstSnapshot, event).pipe(Effect.ignore) : Effect.void, ).pipe(Effect.forkIn(firstScope)); yield* Stream.runForEach(broadcaster.streamStatus({ cwd: "/repo" }), (event) => event._tag === "snapshot" ? Deferred.succeed(secondSnapshot, event).pipe(Effect.ignore) : Effect.void, ).pipe(Effect.forkIn(secondScope)); yield* Deferred.await(firstSnapshot); yield* Deferred.await(secondSnapshot); yield* Deferred.await(remoteStarted); assert.equal(state.remoteStatusCalls, 1); yield* Scope.close(firstScope, Exit.void); assert.isTrue(Option.isNone(yield* Deferred.poll(remoteInterrupted))); yield* Scope.close(secondScope, Exit.void).pipe(Effect.forkScoped); yield* Deferred.await(remoteInterrupted); assert.isTrue(Option.isSome(yield* Deferred.poll(remoteInterrupted))); }).pipe(Effect.provide(testLayer)); });});
import { assert, it, describe } from "@effect/vitest";
import * as NodeServices from "@effect/platform-node/NodeServices";
import * as Deferred from "effect/Deferred";
import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Path from "effect/Path";
import * as Scope from "effect/Scope";
import * as Stream from "effect/Stream";
import type {
VcsStatusLocalResult,
VcsStatusRemoteResult,
VcsStatusResult,
VcsStatusStreamEvent,
} from "@t3tools/contracts";
import * as VcsStatusBroadcaster from "./VcsStatusBroadcaster.ts";
import * as GitWorkflowService from "../git/GitWorkflowService.ts";
const baseLocalStatus: VcsStatusLocalResult = {
isRepo: true,
sourceControlProvider: {
kind: "github",
name: "GitHub",
baseUrl: "https://github.com",
},
hasPrimaryRemote: true,
isDefaultRef: false,
refName: "feature/status-broadcast",
hasWorkingTreeChanges: false,
workingTree: { files: [], insertions: 0, deletions: 0 },
};
const baseRemoteStatus: VcsStatusRemoteResult = {
hasUpstream: true,
aheadCount: 0,
behindCount: 0,
pr: null,
};
const baseStatus: VcsStatusResult = {
...baseLocalStatus,
...baseRemoteStatus,
};
function makeTestLayer(state: {
currentLocalStatus: VcsStatusLocalResult;
currentRemoteStatus: VcsStatusRemoteResult | null;
localStatusCalls: number;
remoteStatusCalls: number;
localInvalidationCalls: number;
remoteInvalidationCalls: number;
}) {
return VcsStatusBroadcaster.layer.pipe(
Layer.provideMerge(NodeServices.layer),
Layer.provide(
Layer.mock(GitWorkflowService.GitWorkflowService)({
localStatus: () =>
Effect.sync(() => {
state.localStatusCalls += 1;
return state.currentLocalStatus;
}),
remoteStatus: () =>
Effect.sync(() => {
state.remoteStatusCalls += 1;
return state.currentRemoteStatus;
}),
invalidateLocalStatus: () =>
Effect.sync(() => {
state.localInvalidationCalls += 1;
}),
invalidateRemoteStatus: () =>
Effect.sync(() => {
state.remoteInvalidationCalls += 1;
}),
}),
),
);
}
describe("VcsStatusBroadcaster", () => {
it.effect("reuses the cached VCS status across repeated reads", () => {
const state = {
currentLocalStatus: baseLocalStatus,
currentRemoteStatus: baseRemoteStatus,
localStatusCalls: 0,
remoteStatusCalls: 0,
localInvalidationCalls: 0,
remoteInvalidationCalls: 0,
};
return Effect.gen(function* () {
const broadcaster = yield* VcsStatusBroadcaster.VcsStatusBroadcaster;
const first = yield* broadcaster.getStatus({ cwd: "/repo" });
const second = yield* broadcaster.getStatus({ cwd: "/repo" });
assert.deepStrictEqual(first, baseStatus);
assert.deepStrictEqual(second, baseStatus);
assert.equal(state.localStatusCalls, 1);
assert.equal(state.remoteStatusCalls, 1);
assert.equal(state.localInvalidationCalls, 0);
assert.equal(state.remoteInvalidationCalls, 0);
}).pipe(Effect.provide(makeTestLayer(state)));
});
it.effect("refreshes the cached snapshot after explicit invalidation", () => {
const state = {
currentLocalStatus: baseLocalStatus,
currentRemoteStatus: baseRemoteStatus,
localStatusCalls: 0,
remoteStatusCalls: 0,
localInvalidationCalls: 0,
remoteInvalidationCalls: 0,
};
return Effect.gen(function* () {
const broadcaster = yield* VcsStatusBroadcaster.VcsStatusBroadcaster;
const initial = yield* broadcaster.getStatus({ cwd: "/repo" });
state.currentLocalStatus = {
...baseLocalStatus,
refName: "feature/updated-status",
};
state.currentRemoteStatus = {
...baseRemoteStatus,
aheadCount: 2,
};
const refreshed = yield* broadcaster.refreshStatus("/repo");
const cached = yield* broadcaster.getStatus({ cwd: "/repo" });
assert.deepStrictEqual(initial, baseStatus);
assert.deepStrictEqual(refreshed, {
...state.currentLocalStatus,
...state.currentRemoteStatus,
});
assert.deepStrictEqual(cached, {
...state.currentLocalStatus,
...state.currentRemoteStatus,
});
assert.equal(state.localStatusCalls, 2);
assert.equal(state.remoteStatusCalls, 2);
assert.equal(state.localInvalidationCalls, 1);
assert.equal(state.remoteInvalidationCalls, 1);
}).pipe(Effect.provide(makeTestLayer(state)));
});
it.effect("refreshes only the cached local snapshot when requested", () => {
const state = {
currentLocalStatus: baseLocalStatus,
currentRemoteStatus: baseRemoteStatus,
localStatusCalls: 0,
remoteStatusCalls: 0,
localInvalidationCalls: 0,
remoteInvalidationCalls: 0,
};
return Effect.gen(function* () {
const broadcaster = yield* VcsStatusBroadcaster.VcsStatusBroadcaster;
const initial = yield* broadcaster.getStatus({ cwd: "/repo" });
state.currentLocalStatus = {
...baseLocalStatus,
refName: "feature/local-only-refresh",
hasWorkingTreeChanges: true,
};
const refreshedLocal = yield* broadcaster.refreshLocalStatus("/repo");
const cached = yield* broadcaster.getStatus({ cwd: "/repo" });
assert.deepStrictEqual(initial, baseStatus);
assert.deepStrictEqual(refreshedLocal, state.currentLocalStatus);
assert.deepStrictEqual(cached, {
...state.currentLocalStatus,
...baseRemoteStatus,
});
assert.equal(state.localStatusCalls, 2);
assert.equal(state.remoteStatusCalls, 1);
assert.equal(state.localInvalidationCalls, 1);
assert.equal(state.remoteInvalidationCalls, 0);
}).pipe(Effect.provide(makeTestLayer(state)));
});
it.effect("normalizes symlinked CWDs before cache lookup and workflow calls", () => {
const seenCwds: string[] = [];
const state = {
currentLocalStatus: baseLocalStatus,
currentRemoteStatus: baseRemoteStatus,
localStatusCalls: 0,
remoteStatusCalls: 0,
localInvalidationCalls: 0,
remoteInvalidationCalls: 0,
};
const testLayer = VcsStatusBroadcaster.layer.pipe(
Layer.provideMerge(NodeServices.layer),
Layer.provide(
Layer.mock(GitWorkflowService.GitWorkflowService)({
localStatus: (input) =>
Effect.sync(() => {
seenCwds.push(input.cwd);
state.localStatusCalls += 1;
return state.currentLocalStatus;
}),
remoteStatus: (input) =>
Effect.sync(() => {
seenCwds.push(input.cwd);
state.remoteStatusCalls += 1;
return state.currentRemoteStatus;
}),
invalidateLocalStatus: () =>
Effect.sync(() => {
state.localInvalidationCalls += 1;
}),
invalidateRemoteStatus: () =>
Effect.sync(() => {
state.remoteInvalidationCalls += 1;
}),
} satisfies Partial<GitWorkflowService.GitWorkflowServiceShape>),
),
);
return Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const realDir = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-vcs-status-real-",
});
const linkParent = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-vcs-status-link-",
});
const linkDir = path.join(linkParent, "repo-link");
yield* fileSystem.symlink(realDir, linkDir);
const realPath = yield* fileSystem.realPath(realDir);
const broadcaster = yield* VcsStatusBroadcaster.VcsStatusBroadcaster;
yield* broadcaster.getStatus({ cwd: linkDir });
yield* broadcaster.getStatus({ cwd: realDir });
assert.deepStrictEqual(seenCwds, [realPath, realPath]);
assert.equal(state.localStatusCalls, 1);
assert.equal(state.remoteStatusCalls, 1);
}).pipe(Effect.provide(testLayer));
});
it.effect("streams a local snapshot first and remote updates later", () => {
const state = {
currentLocalStatus: baseLocalStatus,
currentRemoteStatus: baseRemoteStatus,
localStatusCalls: 0,
remoteStatusCalls: 0,
localInvalidationCalls: 0,
remoteInvalidationCalls: 0,
};
return Effect.gen(function* () {
const broadcaster = yield* VcsStatusBroadcaster.VcsStatusBroadcaster;
const snapshotDeferred = yield* Deferred.make<VcsStatusStreamEvent>();
const remoteUpdatedDeferred = yield* Deferred.make<VcsStatusStreamEvent>();
yield* Stream.runForEach(broadcaster.streamStatus({ cwd: "/repo" }), (event) => {
if (event._tag === "snapshot") {
return Deferred.succeed(snapshotDeferred, event).pipe(Effect.ignore);
}
if (event._tag === "remoteUpdated") {
return Deferred.succeed(remoteUpdatedDeferred, event).pipe(Effect.ignore);
}
return Effect.void;
}).pipe(Effect.forkScoped);
const snapshot = yield* Deferred.await(snapshotDeferred);
yield* broadcaster.refreshStatus("/repo");
const remoteUpdated = yield* Deferred.await(remoteUpdatedDeferred);
assert.deepStrictEqual(snapshot, {
_tag: "snapshot",
local: baseLocalStatus,
remote: null,
} satisfies VcsStatusStreamEvent);
assert.deepStrictEqual(remoteUpdated, {
_tag: "remoteUpdated",
remote: baseRemoteStatus,
} satisfies VcsStatusStreamEvent);
}).pipe(Effect.provide(makeTestLayer(state)));
});
it.effect("does not start automatic remote refreshes when disabled", () => {
const state = {
currentLocalStatus: baseLocalStatus,
currentRemoteStatus: baseRemoteStatus,
localStatusCalls: 0,
remoteStatusCalls: 0,
localInvalidationCalls: 0,
remoteInvalidationCalls: 0,
};
return Effect.gen(function* () {
const broadcaster = yield* VcsStatusBroadcaster.VcsStatusBroadcaster;
const snapshot = yield* Stream.runHead(
broadcaster.streamStatus(
{ cwd: "/repo" },
{ automaticRemoteRefreshInterval: Effect.succeed(Duration.zero) },
),
);
assert.isTrue(Option.isSome(snapshot));
assert.equal(state.remoteStatusCalls, 0);
assert.equal(state.remoteInvalidationCalls, 0);
}).pipe(Effect.provide(makeTestLayer(state)));
});
it("backs off remote refresh failures exponentially and honors larger configured intervals", () => {
assert.equal(
Duration.toMillis(VcsStatusBroadcaster.remoteRefreshFailureDelay(1, Duration.seconds(1))),
30_000,
);
assert.equal(
Duration.toMillis(VcsStatusBroadcaster.remoteRefreshFailureDelay(2, Duration.seconds(1))),
60_000,
);
assert.equal(
Duration.toMillis(VcsStatusBroadcaster.remoteRefreshFailureDelay(3, Duration.seconds(1))),
120_000,
);
assert.equal(
Duration.toMillis(VcsStatusBroadcaster.remoteRefreshFailureDelay(1, Duration.minutes(5))),
300_000,
);
assert.equal(
Duration.toMillis(VcsStatusBroadcaster.remoteRefreshFailureDelay(20, Duration.seconds(1))),
900_000,
);
});
it.effect("stops the remote poller after the last stream subscriber disconnects", () => {
const state = {
currentLocalStatus: baseLocalStatus,
currentRemoteStatus: baseRemoteStatus,
localStatusCalls: 0,
remoteStatusCalls: 0,
localInvalidationCalls: 0,
remoteInvalidationCalls: 0,
};
let remoteInterruptedDeferred: Deferred.Deferred<void, never> | null = null;
let remoteStartedDeferred: Deferred.Deferred<void, never> | null = null;
const testLayer = VcsStatusBroadcaster.layer.pipe(
Layer.provideMerge(NodeServices.layer),
Layer.provide(
Layer.mock(GitWorkflowService.GitWorkflowService)({
localStatus: () =>
Effect.sync(() => {
state.localStatusCalls += 1;
return state.currentLocalStatus;
}),
remoteStatus: () =>
Effect.sync(() => {
state.remoteStatusCalls += 1;
}).pipe(
Effect.andThen(
remoteStartedDeferred
? Deferred.succeed(remoteStartedDeferred, undefined).pipe(Effect.ignore)
: Effect.void,
),
Effect.andThen(Effect.never as Effect.Effect<VcsStatusRemoteResult | null, never>),
Effect.onInterrupt(() =>
remoteInterruptedDeferred
? Deferred.succeed(remoteInterruptedDeferred, undefined).pipe(Effect.ignore)
: Effect.void,
),
),
invalidateLocalStatus: () =>
Effect.sync(() => {
state.localInvalidationCalls += 1;
}),
invalidateRemoteStatus: () =>
Effect.sync(() => {
state.remoteInvalidationCalls += 1;
}),
} satisfies Partial<GitWorkflowService.GitWorkflowServiceShape>),
),
);
return Effect.gen(function* () {
const remoteInterrupted = yield* Deferred.make<void>();
const remoteStarted = yield* Deferred.make<void>();
remoteInterruptedDeferred = remoteInterrupted;
remoteStartedDeferred = remoteStarted;
const broadcaster = yield* VcsStatusBroadcaster.VcsStatusBroadcaster;
const firstSnapshot = yield* Deferred.make<VcsStatusStreamEvent>();
const secondSnapshot = yield* Deferred.make<VcsStatusStreamEvent>();
const firstScope = yield* Scope.make();
const secondScope = yield* Scope.make();
yield* Stream.runForEach(broadcaster.streamStatus({ cwd: "/repo" }), (event) =>
event._tag === "snapshot"
? Deferred.succeed(firstSnapshot, event).pipe(Effect.ignore)
: Effect.void,
).pipe(Effect.forkIn(firstScope));
yield* Stream.runForEach(broadcaster.streamStatus({ cwd: "/repo" }), (event) =>
event._tag === "snapshot"
? Deferred.succeed(secondSnapshot, event).pipe(Effect.ignore)
: Effect.void,
).pipe(Effect.forkIn(secondScope));
yield* Deferred.await(firstSnapshot);
yield* Deferred.await(secondSnapshot);
yield* Deferred.await(remoteStarted);
assert.equal(state.remoteStatusCalls, 1);
yield* Scope.close(firstScope, Exit.void);
assert.isTrue(Option.isNone(yield* Deferred.poll(remoteInterrupted)));
yield* Scope.close(secondScope, Exit.void).pipe(Effect.forkScoped);
yield* Deferred.await(remoteInterrupted);
assert.isTrue(Option.isSome(yield* Deferred.poll(remoteInterrupted)));
}).pipe(Effect.provide(testLayer));
});
});