fix(ci): clean member work sync validation
This commit is contained in:
parent
e634895a08
commit
7d08205a23
40 changed files with 194 additions and 169 deletions
|
|
@ -1,2 +1,2 @@
|
|||
export * from './ipc';
|
||||
export * from './types';
|
||||
export type * from './types';
|
||||
|
|
|
|||
|
|
@ -221,11 +221,11 @@ export interface MemberWorkSyncNudgePayload {
|
|||
source: 'member-work-sync';
|
||||
actionMode: 'do';
|
||||
text: string;
|
||||
taskRefs: Array<{
|
||||
taskRefs: {
|
||||
taskId: string;
|
||||
displayId: string;
|
||||
teamName: string;
|
||||
}>;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface MemberWorkSyncOutboxItem {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { MemberWorkSyncStatus, MemberWorkSyncStatusRequest } from '../../contracts';
|
||||
import { MemberWorkSyncReconciler } from './MemberWorkSyncReconciler';
|
||||
|
||||
import type { MemberWorkSyncStatus, MemberWorkSyncStatusRequest } from '../../contracts';
|
||||
import type { MemberWorkSyncUseCaseDeps } from './ports';
|
||||
|
||||
export class MemberWorkSyncDiagnosticsReader {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { MemberWorkSyncMetricsRequest, MemberWorkSyncTeamMetrics } from '../../contracts';
|
||||
import { assessMemberWorkSyncPhase2Readiness } from '../domain';
|
||||
|
||||
import type { MemberWorkSyncMetricsRequest, MemberWorkSyncTeamMetrics } from '../../contracts';
|
||||
import type { MemberWorkSyncUseCaseDeps } from './ports';
|
||||
|
||||
function emptyMetrics(teamName: string, generatedAt: string): MemberWorkSyncTeamMetrics {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { MemberWorkSyncStatus } from '../../contracts';
|
||||
import { buildMemberWorkSyncOutboxEnsureInput } from '../domain';
|
||||
|
||||
import type { MemberWorkSyncStatus } from '../../contracts';
|
||||
import type { MemberWorkSyncUseCaseDeps } from './ports';
|
||||
|
||||
export interface MemberWorkSyncNudgeOutboxPlanResult {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { MemberWorkSyncReportIntentStatus } from '../../contracts';
|
||||
import { MemberWorkSyncReporter } from './MemberWorkSyncReporter';
|
||||
|
||||
import type { MemberWorkSyncReportIntentStatus } from '../../contracts';
|
||||
import type { MemberWorkSyncUseCaseDeps } from './ports';
|
||||
|
||||
export interface MemberWorkSyncPendingReportReplaySummary {
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@ import {
|
|||
decideMemberWorkSyncStatus,
|
||||
formatAgendaFingerprint,
|
||||
} from '../domain';
|
||||
|
||||
import { MemberWorkSyncNudgeOutboxPlanner } from './MemberWorkSyncNudgeOutboxPlanner';
|
||||
|
||||
import type { MemberWorkSyncStatus, MemberWorkSyncStatusRequest } from '../../contracts';
|
||||
import type { MemberWorkSyncAgendaSourceResult, MemberWorkSyncUseCaseDeps } from './ports';
|
||||
import { MemberWorkSyncNudgeOutboxPlanner } from './MemberWorkSyncNudgeOutboxPlanner';
|
||||
|
||||
export interface MemberWorkSyncReconcileContext {
|
||||
reconciledBy?: 'request' | 'queue';
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
import { validateMemberWorkSyncReport } from '../domain';
|
||||
|
||||
import {
|
||||
attachMemberWorkSyncReportToken,
|
||||
finalizeMemberWorkSyncAgenda,
|
||||
MemberWorkSyncReconciler,
|
||||
} from './MemberWorkSyncReconciler';
|
||||
|
||||
import type {
|
||||
MemberWorkSyncReport,
|
||||
MemberWorkSyncReportRequest,
|
||||
MemberWorkSyncReportResult,
|
||||
MemberWorkSyncStatus,
|
||||
} from '../../contracts';
|
||||
import { validateMemberWorkSyncReport } from '../domain';
|
||||
import {
|
||||
attachMemberWorkSyncReportToken,
|
||||
finalizeMemberWorkSyncAgenda,
|
||||
MemberWorkSyncReconciler,
|
||||
} from './MemberWorkSyncReconciler';
|
||||
import type { MemberWorkSyncUseCaseDeps } from './ports';
|
||||
|
||||
export class MemberWorkSyncReporter {
|
||||
|
|
@ -28,7 +30,11 @@ export class MemberWorkSyncReporter {
|
|||
: true;
|
||||
if (!teamActive) {
|
||||
const status = await this.reconciler.execute(request);
|
||||
const rejectedStatus = await this.recordRejectedReport(status, request, 'team_runtime_inactive');
|
||||
const rejectedStatus = await this.recordRejectedReport(
|
||||
status,
|
||||
request,
|
||||
'team_runtime_inactive'
|
||||
);
|
||||
return {
|
||||
accepted: false,
|
||||
code: 'team_runtime_inactive',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,6 @@ export * from './MemberWorkSyncNudgeOutboxPlanner';
|
|||
export * from './MemberWorkSyncPendingReportIntentReplayer';
|
||||
export * from './MemberWorkSyncReconciler';
|
||||
export * from './MemberWorkSyncReporter';
|
||||
export * from './ports';
|
||||
export type * from './ports';
|
||||
export * from './RuntimeTurnSettledIngestor';
|
||||
export * from './RuntimeTurnSettledPorts';
|
||||
export type * from './RuntimeTurnSettledPorts';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import type {
|
||||
MemberWorkSyncAgenda,
|
||||
MemberWorkSyncTeamMetrics,
|
||||
MemberWorkSyncProviderId,
|
||||
MemberWorkSyncOutboxClaimInput,
|
||||
MemberWorkSyncOutboxCountRecentDeliveredInput,
|
||||
MemberWorkSyncOutboxEnsureInput,
|
||||
|
|
@ -10,11 +8,13 @@ import type {
|
|||
MemberWorkSyncOutboxMarkDeliveredInput,
|
||||
MemberWorkSyncOutboxMarkFailedInput,
|
||||
MemberWorkSyncOutboxMarkSupersededInput,
|
||||
MemberWorkSyncProviderId,
|
||||
MemberWorkSyncReport,
|
||||
MemberWorkSyncReportIntent,
|
||||
MemberWorkSyncReportIntentStatus,
|
||||
MemberWorkSyncReportRequest,
|
||||
MemberWorkSyncStatus,
|
||||
MemberWorkSyncTeamMetrics,
|
||||
} from '../../contracts';
|
||||
|
||||
export interface MemberWorkSyncClockPort {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,3 @@
|
|||
import type {
|
||||
MemberWorkSyncActionableWorkItem,
|
||||
MemberWorkSyncAgenda,
|
||||
MemberWorkSyncProviderId,
|
||||
} from '../../contracts';
|
||||
import {
|
||||
buildAgendaFingerprintPayload,
|
||||
canonicalizeAgendaFingerprintPayload,
|
||||
|
|
@ -11,6 +6,12 @@ import {
|
|||
import { resolveCurrentReviewOwner, type ReviewHistoryEventLike } from './currentReviewCycle';
|
||||
import { isReservedMemberName, normalizeMemberName, sameMemberName } from './memberName';
|
||||
|
||||
import type {
|
||||
MemberWorkSyncActionableWorkItem,
|
||||
MemberWorkSyncAgenda,
|
||||
MemberWorkSyncProviderId,
|
||||
} from '../../contracts';
|
||||
|
||||
export interface MemberWorkSyncTaskLike {
|
||||
id: string;
|
||||
displayId?: string;
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export interface AgendaFingerprintPayload {
|
|||
version: 1;
|
||||
teamName: string;
|
||||
memberName: string;
|
||||
items: Array<{
|
||||
items: {
|
||||
taskId: string;
|
||||
displayId?: string;
|
||||
subject: string;
|
||||
|
|
@ -27,7 +27,7 @@ export interface AgendaFingerprintPayload {
|
|||
assignee: string;
|
||||
priority: string;
|
||||
evidence: MemberWorkSyncActionableWorkItem['evidence'];
|
||||
}>;
|
||||
}[];
|
||||
sourceRevision?: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { isReservedMemberName, normalizeMemberName, sameMemberName } from './memberName';
|
||||
|
||||
import type {
|
||||
MemberWorkSyncAgenda,
|
||||
MemberWorkSyncReportRequest,
|
||||
MemberWorkSyncReportState,
|
||||
} from '../../contracts';
|
||||
import { isReservedMemberName, normalizeMemberName, sameMemberName } from './memberName';
|
||||
|
||||
export interface MemberWorkSyncReportValidation {
|
||||
ok: boolean;
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ export * from './ActionableWorkAgenda';
|
|||
export * from './AgendaFingerprint';
|
||||
export * from './currentReviewCycle';
|
||||
export * from './memberName';
|
||||
export * from './MemberWorkSyncPhase2Readiness';
|
||||
export * from './MemberWorkSyncNudge';
|
||||
export * from './MemberWorkSyncPhase2Readiness';
|
||||
export * from './MemberWorkSyncReportValidator';
|
||||
export * from './RuntimeTurnSettledEvent';
|
||||
export * from './RuntimeTurnSettledProvider';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { createLogger } from '@shared/utils/logger';
|
||||
|
||||
import {
|
||||
MEMBER_WORK_SYNC_GET_METRICS,
|
||||
MEMBER_WORK_SYNC_GET_STATUS,
|
||||
|
|
@ -9,7 +11,6 @@ import {
|
|||
type MemberWorkSyncStatusRequest,
|
||||
type MemberWorkSyncTeamMetrics,
|
||||
} from '../../../contracts';
|
||||
import { createLogger } from '@shared/utils/logger';
|
||||
|
||||
import type { MemberWorkSyncFeatureFacade } from '../../composition/createMemberWorkSyncFeature';
|
||||
import type { IpcMain } from 'electron';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { TeamInboxReader } from '@main/services/team/TeamInboxReader';
|
||||
import { TeamInboxWriter } from '@main/services/team/TeamInboxWriter';
|
||||
|
||||
import type { MemberWorkSyncInboxNudgePort } from '../../../core/application';
|
||||
|
||||
export class TeamInboxMemberWorkSyncNudgeSink implements MemberWorkSyncInboxNudgePort {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
import path from 'path';
|
||||
|
||||
import { isReservedMemberName, normalizeMemberName } from '../../../core/domain';
|
||||
import type {
|
||||
RuntimeTurnSettledTargetResolution,
|
||||
RuntimeTurnSettledTargetResolverPort,
|
||||
} from '../../../core/application';
|
||||
import { TeamMemberLogsFinder } from '@main/services/team/TeamMemberLogsFinder';
|
||||
import {
|
||||
inferTeamProviderIdFromModel,
|
||||
normalizeOptionalTeamProviderId,
|
||||
} from '@shared/utils/teamProvider';
|
||||
import path from 'path';
|
||||
|
||||
import { isReservedMemberName, normalizeMemberName } from '../../../core/domain';
|
||||
|
||||
import type {
|
||||
RuntimeTurnSettledTargetResolution,
|
||||
RuntimeTurnSettledTargetResolverPort,
|
||||
} from '../../../core/application';
|
||||
import type { RuntimeTurnSettledEvent } from '../../../core/domain';
|
||||
import type { TeamConfigReader } from '@main/services/team/TeamConfigReader';
|
||||
import type { TeamMembersMetaStore } from '@main/services/team/TeamMembersMetaStore';
|
||||
|
|
@ -69,9 +69,7 @@ function normalizePath(value: string | undefined): string | null {
|
|||
return path.resolve(value.trim());
|
||||
}
|
||||
|
||||
export class TeamRuntimeTurnSettledTargetResolver
|
||||
implements RuntimeTurnSettledTargetResolverPort
|
||||
{
|
||||
export class TeamRuntimeTurnSettledTargetResolver implements RuntimeTurnSettledTargetResolverPort {
|
||||
private readonly memberLogsFinder: AttributedMemberFileSource;
|
||||
private readonly maxTeamsToScan: number;
|
||||
|
||||
|
|
@ -99,12 +97,12 @@ export class TeamRuntimeTurnSettledTargetResolver
|
|||
.filter((team) => !team.deletedAt)
|
||||
.slice(0, this.maxTeamsToScan);
|
||||
|
||||
const candidates: Array<{
|
||||
const candidates: {
|
||||
teamName: string;
|
||||
memberName: string;
|
||||
exactPath: boolean;
|
||||
mtimeMs: number;
|
||||
}> = [];
|
||||
}[] = [];
|
||||
|
||||
for (const team of teams) {
|
||||
const attributedFiles = await this.memberLogsFinder
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
import {
|
||||
buildActionableWorkAgenda,
|
||||
isReservedMemberName,
|
||||
normalizeMemberName,
|
||||
type MemberWorkSyncMemberLike,
|
||||
} from '../../../core/domain';
|
||||
import type {
|
||||
MemberWorkSyncAgendaSourcePort,
|
||||
MemberWorkSyncAgendaSourceResult,
|
||||
MemberWorkSyncHashPort,
|
||||
} from '../../../core/application';
|
||||
import {
|
||||
inferTeamProviderIdFromModel,
|
||||
normalizeOptionalTeamProviderId,
|
||||
} from '@shared/utils/teamProvider';
|
||||
|
||||
import {
|
||||
buildActionableWorkAgenda,
|
||||
isReservedMemberName,
|
||||
type MemberWorkSyncMemberLike,
|
||||
normalizeMemberName,
|
||||
} from '../../../core/domain';
|
||||
|
||||
import type {
|
||||
MemberWorkSyncAgendaSourcePort,
|
||||
MemberWorkSyncAgendaSourceResult,
|
||||
MemberWorkSyncHashPort,
|
||||
} from '../../../core/application';
|
||||
import type { TeamConfigReader } from '@main/services/team/TeamConfigReader';
|
||||
import type { TeamKanbanManager } from '@main/services/team/TeamKanbanManager';
|
||||
import type { TeamMembersMetaStore } from '@main/services/team/TeamMembersMetaStore';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { readFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
|
||||
import type { MemberWorkSyncWatchdogCooldownPort } from '../../../core/application';
|
||||
|
||||
const DEFAULT_WATCHDOG_COOLDOWN_MS = 10 * 60_000;
|
||||
|
|
@ -54,10 +55,7 @@ export class TeamTaskStallJournalWorkSyncCooldown implements MemberWorkSyncWatch
|
|||
return alertedAt != null && now - alertedAt <= this.cooldownMs;
|
||||
});
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return (error as NodeJS.ErrnoException).code !== 'ENOENT';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,43 @@
|
|||
import {
|
||||
MemberWorkSyncDiagnosticsReader,
|
||||
MemberWorkSyncMetricsReader,
|
||||
MemberWorkSyncNudgeDispatcher,
|
||||
type MemberWorkSyncNudgeDispatchSummary,
|
||||
MemberWorkSyncPendingReportIntentReplayer,
|
||||
type MemberWorkSyncPendingReportReplaySummary,
|
||||
type MemberWorkSyncReconcileContext,
|
||||
MemberWorkSyncReconciler,
|
||||
MemberWorkSyncReporter,
|
||||
type RuntimeTurnSettledDrainSummary,
|
||||
RuntimeTurnSettledIngestor,
|
||||
type RuntimeTurnSettledTargetResolverPort,
|
||||
} from '../../core/application';
|
||||
import { MemberWorkSyncTeamChangeRouter } from '../adapters/input/MemberWorkSyncTeamChangeRouter';
|
||||
import { TeamInboxMemberWorkSyncNudgeSink } from '../adapters/output/TeamInboxMemberWorkSyncNudgeSink';
|
||||
import { TeamRuntimeTurnSettledTargetResolver } from '../adapters/output/TeamRuntimeTurnSettledTargetResolver';
|
||||
import { TeamTaskAgendaSource } from '../adapters/output/TeamTaskAgendaSource';
|
||||
import { TeamTaskStallJournalWorkSyncCooldown } from '../adapters/output/TeamTaskStallJournalWorkSyncCooldown';
|
||||
import { ClaudeStopHookPayloadNormalizer } from '../infrastructure/ClaudeStopHookPayloadNormalizer';
|
||||
import { CodexNativeTurnSettledPayloadNormalizer } from '../infrastructure/CodexNativeTurnSettledPayloadNormalizer';
|
||||
import { CompositeRuntimeTurnSettledPayloadNormalizer } from '../infrastructure/CompositeRuntimeTurnSettledPayloadNormalizer';
|
||||
import { FileRuntimeTurnSettledEventStore } from '../infrastructure/FileRuntimeTurnSettledEventStore';
|
||||
import { HmacMemberWorkSyncReportTokenAdapter } from '../infrastructure/HmacMemberWorkSyncReportTokenAdapter';
|
||||
import { JsonMemberWorkSyncStore } from '../infrastructure/JsonMemberWorkSyncStore';
|
||||
import {
|
||||
MemberWorkSyncEventQueue,
|
||||
type MemberWorkSyncQueueDiagnostics,
|
||||
} from '../infrastructure/MemberWorkSyncEventQueue';
|
||||
import { MemberWorkSyncNudgeDispatchScheduler } from '../infrastructure/MemberWorkSyncNudgeDispatchScheduler';
|
||||
import { MemberWorkSyncStorePaths } from '../infrastructure/MemberWorkSyncStorePaths';
|
||||
import { MemberWorkSyncToolActivityBusySignal } from '../infrastructure/MemberWorkSyncToolActivityBusySignal';
|
||||
import { NodeHashAdapter } from '../infrastructure/NodeHashAdapter';
|
||||
import { RuntimeTurnSettledDrainScheduler } from '../infrastructure/RuntimeTurnSettledDrainScheduler';
|
||||
import { buildRuntimeTurnSettledEnvironment } from '../infrastructure/runtimeTurnSettledEnvironment';
|
||||
import { buildRuntimeTurnSettledHookSettings } from '../infrastructure/runtimeTurnSettledHookSettings';
|
||||
import { RuntimeTurnSettledSpoolPaths } from '../infrastructure/RuntimeTurnSettledSpoolPaths';
|
||||
import { ShellRuntimeTurnSettledHookScriptInstaller } from '../infrastructure/ShellRuntimeTurnSettledHookScriptInstaller';
|
||||
import { SystemClockAdapter } from '../infrastructure/SystemClockAdapter';
|
||||
|
||||
import type {
|
||||
MemberWorkSyncMetricsRequest,
|
||||
MemberWorkSyncReportRequest,
|
||||
|
|
@ -6,53 +46,13 @@ import type {
|
|||
MemberWorkSyncStatusRequest,
|
||||
MemberWorkSyncTeamMetrics,
|
||||
} from '../../contracts';
|
||||
import {
|
||||
MemberWorkSyncDiagnosticsReader,
|
||||
MemberWorkSyncMetricsReader,
|
||||
MemberWorkSyncNudgeDispatcher,
|
||||
type MemberWorkSyncNudgeDispatchSummary,
|
||||
MemberWorkSyncPendingReportIntentReplayer,
|
||||
type MemberWorkSyncPendingReportReplaySummary,
|
||||
MemberWorkSyncReconciler,
|
||||
MemberWorkSyncReporter,
|
||||
type MemberWorkSyncReconcileContext,
|
||||
RuntimeTurnSettledIngestor,
|
||||
type RuntimeTurnSettledDrainSummary,
|
||||
type RuntimeTurnSettledTargetResolverPort,
|
||||
} from '../../core/application';
|
||||
import { MemberWorkSyncTeamChangeRouter } from '../adapters/input/MemberWorkSyncTeamChangeRouter';
|
||||
import { TeamInboxMemberWorkSyncNudgeSink } from '../adapters/output/TeamInboxMemberWorkSyncNudgeSink';
|
||||
import { TeamTaskStallJournalWorkSyncCooldown } from '../adapters/output/TeamTaskStallJournalWorkSyncCooldown';
|
||||
import { TeamTaskAgendaSource } from '../adapters/output/TeamTaskAgendaSource';
|
||||
import { TeamRuntimeTurnSettledTargetResolver } from '../adapters/output/TeamRuntimeTurnSettledTargetResolver';
|
||||
import { ClaudeStopHookPayloadNormalizer } from '../infrastructure/ClaudeStopHookPayloadNormalizer';
|
||||
import { CodexNativeTurnSettledPayloadNormalizer } from '../infrastructure/CodexNativeTurnSettledPayloadNormalizer';
|
||||
import { CompositeRuntimeTurnSettledPayloadNormalizer } from '../infrastructure/CompositeRuntimeTurnSettledPayloadNormalizer';
|
||||
import { FileRuntimeTurnSettledEventStore } from '../infrastructure/FileRuntimeTurnSettledEventStore';
|
||||
import { HmacMemberWorkSyncReportTokenAdapter } from '../infrastructure/HmacMemberWorkSyncReportTokenAdapter';
|
||||
import {
|
||||
MemberWorkSyncEventQueue,
|
||||
type MemberWorkSyncQueueDiagnostics,
|
||||
} from '../infrastructure/MemberWorkSyncEventQueue';
|
||||
import { JsonMemberWorkSyncStore } from '../infrastructure/JsonMemberWorkSyncStore';
|
||||
import { MemberWorkSyncStorePaths } from '../infrastructure/MemberWorkSyncStorePaths';
|
||||
import { MemberWorkSyncNudgeDispatchScheduler } from '../infrastructure/MemberWorkSyncNudgeDispatchScheduler';
|
||||
import { MemberWorkSyncToolActivityBusySignal } from '../infrastructure/MemberWorkSyncToolActivityBusySignal';
|
||||
import { NodeHashAdapter } from '../infrastructure/NodeHashAdapter';
|
||||
import { RuntimeTurnSettledDrainScheduler } from '../infrastructure/RuntimeTurnSettledDrainScheduler';
|
||||
import { RuntimeTurnSettledSpoolPaths } from '../infrastructure/RuntimeTurnSettledSpoolPaths';
|
||||
import { ShellRuntimeTurnSettledHookScriptInstaller } from '../infrastructure/ShellRuntimeTurnSettledHookScriptInstaller';
|
||||
import { buildRuntimeTurnSettledEnvironment } from '../infrastructure/runtimeTurnSettledEnvironment';
|
||||
import { buildRuntimeTurnSettledHookSettings } from '../infrastructure/runtimeTurnSettledHookSettings';
|
||||
import { SystemClockAdapter } from '../infrastructure/SystemClockAdapter';
|
||||
|
||||
import type { MemberWorkSyncLoggerPort } from '../../core/application';
|
||||
import type { RuntimeTurnSettledProvider } from '../../core/domain';
|
||||
import type { TeamConfigReader } from '@main/services/team/TeamConfigReader';
|
||||
import type { TeamKanbanManager } from '@main/services/team/TeamKanbanManager';
|
||||
import type { TeamMembersMetaStore } from '@main/services/team/TeamMembersMetaStore';
|
||||
import type { TeamTaskReader } from '@main/services/team/TeamTaskReader';
|
||||
import type { TeamChangeEvent } from '@shared/types';
|
||||
import type { MemberWorkSyncLoggerPort } from '../../core/application';
|
||||
import type { RuntimeTurnSettledProvider } from '../../core/domain';
|
||||
|
||||
export const MEMBER_WORK_SYNC_NUDGE_SIDE_EFFECTS_ENV =
|
||||
'CLAUDE_TEAM_MEMBER_WORK_SYNC_NUDGES_ENABLED';
|
||||
|
|
@ -208,17 +208,18 @@ export function createMemberWorkSyncFeature(deps: {
|
|||
drain: () => runtimeTurnSettledIngestor.drainPending(),
|
||||
logger: deps.logger,
|
||||
});
|
||||
const nudgeDispatchScheduler = nudgeSideEffectsEnabled && deps.listLifecycleActiveTeamNames
|
||||
? new MemberWorkSyncNudgeDispatchScheduler({
|
||||
listLifecycleActiveTeamNames: deps.listLifecycleActiveTeamNames,
|
||||
dispatchDue: (teamNames) =>
|
||||
nudgeDispatcher.dispatchDue({
|
||||
teamNames,
|
||||
claimedBy: `member-work-sync:${process.pid}:scheduled`,
|
||||
}),
|
||||
logger: deps.logger,
|
||||
})
|
||||
: null;
|
||||
const nudgeDispatchScheduler =
|
||||
nudgeSideEffectsEnabled && deps.listLifecycleActiveTeamNames
|
||||
? new MemberWorkSyncNudgeDispatchScheduler({
|
||||
listLifecycleActiveTeamNames: deps.listLifecycleActiveTeamNames,
|
||||
dispatchDue: (teamNames) =>
|
||||
nudgeDispatcher.dispatchDue({
|
||||
teamNames,
|
||||
claimedBy: `member-work-sync:${process.pid}:scheduled`,
|
||||
}),
|
||||
logger: deps.logger,
|
||||
})
|
||||
: null;
|
||||
runtimeTurnSettledDrainScheduler.start();
|
||||
nudgeDispatchScheduler?.start();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
export type { RuntimeTurnSettledProvider } from '../core/domain';
|
||||
export {
|
||||
registerMemberWorkSyncIpc,
|
||||
removeMemberWorkSyncIpc,
|
||||
} from './adapters/input/registerMemberWorkSyncIpc';
|
||||
export type { MemberWorkSyncFeatureFacade } from './composition/createMemberWorkSyncFeature';
|
||||
export {
|
||||
MEMBER_WORK_SYNC_NUDGE_SIDE_EFFECTS_ENV,
|
||||
createMemberWorkSyncFeature,
|
||||
MEMBER_WORK_SYNC_NUDGE_SIDE_EFFECTS_ENV,
|
||||
resolveMemberWorkSyncNudgeSideEffectsEnabled,
|
||||
} from './composition/createMemberWorkSyncFeature';
|
||||
export type { MemberWorkSyncFeatureFacade } from './composition/createMemberWorkSyncFeature';
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import {
|
|||
buildRuntimeTurnSettledSourceId,
|
||||
type RuntimeTurnSettledProvider,
|
||||
} from '../../core/domain';
|
||||
|
||||
import type {
|
||||
MemberWorkSyncHashPort,
|
||||
RuntimeTurnSettledPayloadNormalization,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import {
|
|||
buildRuntimeTurnSettledSourceId,
|
||||
type RuntimeTurnSettledProvider,
|
||||
} from '../../core/domain';
|
||||
|
||||
import type {
|
||||
MemberWorkSyncHashPort,
|
||||
RuntimeTurnSettledPayloadNormalization,
|
||||
|
|
@ -28,9 +29,7 @@ function getString(record: Record<string, unknown>, ...keys: string[]): string |
|
|||
return undefined;
|
||||
}
|
||||
|
||||
export class CodexNativeTurnSettledPayloadNormalizer
|
||||
implements RuntimeTurnSettledPayloadNormalizerPort
|
||||
{
|
||||
export class CodexNativeTurnSettledPayloadNormalizer implements RuntimeTurnSettledPayloadNormalizerPort {
|
||||
constructor(private readonly hash: MemberWorkSyncHashPort) {}
|
||||
|
||||
normalize(input: {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { mkdir, readdir, readFile, rename, rm, stat, writeFile } from 'fs/promis
|
|||
import path from 'path';
|
||||
|
||||
import { isRuntimeTurnSettledProvider } from '../../core/domain';
|
||||
|
||||
import type {
|
||||
RuntimeTurnSettledClaimedPayload,
|
||||
RuntimeTurnSettledEventStorePort,
|
||||
|
|
@ -131,7 +132,11 @@ export class FileRuntimeTurnSettledEventStore implements RuntimeTurnSettledEvent
|
|||
): Promise<void> {
|
||||
const processedPath = path.join(this.deps.paths.getProcessedDir(), payload.fileName);
|
||||
await moveFileBestEffort(payload.filePath, processedPath);
|
||||
await writeFile(buildMetaFilePath(processedPath), `${JSON.stringify(result, null, 2)}\n`, 'utf8');
|
||||
await writeFile(
|
||||
buildMetaFilePath(processedPath),
|
||||
`${JSON.stringify(result, null, 2)}\n`,
|
||||
'utf8'
|
||||
);
|
||||
await this.cleanupDirectory(this.deps.paths.getProcessedDir());
|
||||
}
|
||||
|
||||
|
|
@ -164,9 +169,7 @@ export class FileRuntimeTurnSettledEventStore implements RuntimeTurnSettledEvent
|
|||
entries
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.isFile() &&
|
||||
!entry.name.startsWith('.') &&
|
||||
!entry.name.endsWith('.meta.json')
|
||||
entry.isFile() && !entry.name.startsWith('.') && !entry.name.endsWith('.meta.json')
|
||||
)
|
||||
.map(async (entry) => {
|
||||
const processingPath = path.join(this.deps.paths.getProcessingDir(), entry.name);
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import { atomicWriteAsync } from '@main/utils/atomicWrite';
|
|||
import type {
|
||||
MemberWorkSyncReportTokenCreateInput,
|
||||
MemberWorkSyncReportTokenPort,
|
||||
MemberWorkSyncReportTokenVerifyInput,
|
||||
MemberWorkSyncReportTokenVerification,
|
||||
MemberWorkSyncReportTokenVerifyInput,
|
||||
} from '../../core/application';
|
||||
import type { MemberWorkSyncStorePaths } from './MemberWorkSyncStorePaths';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { withFileLock } from '@main/services/team/fileLock';
|
||||
import { atomicWriteAsync } from '@main/utils/atomicWrite';
|
||||
import { createHash } from 'crypto';
|
||||
import { mkdir, readFile, rename } from 'fs/promises';
|
||||
|
||||
import { withFileLock } from '@main/services/team/fileLock';
|
||||
import { assessMemberWorkSyncPhase2Readiness } from '../../core/domain';
|
||||
|
||||
import type {
|
||||
MemberWorkSyncMetricEvent,
|
||||
MemberWorkSyncOutboxClaimInput,
|
||||
|
|
@ -19,7 +21,6 @@ import type {
|
|||
MemberWorkSyncStatusState,
|
||||
MemberWorkSyncTeamMetrics,
|
||||
} from '../../contracts';
|
||||
import { assessMemberWorkSyncPhase2Readiness } from '../../core/domain';
|
||||
import type {
|
||||
MemberWorkSyncOutboxStorePort,
|
||||
MemberWorkSyncReportStorePort,
|
||||
|
|
@ -288,13 +289,10 @@ export class JsonMemberWorkSyncStore
|
|||
stateCounts,
|
||||
actionableItemCount,
|
||||
wouldNudgeCount: recentEvents.filter((event) => event.kind === 'would_nudge').length,
|
||||
fingerprintChangeCount: recentEvents.filter(
|
||||
(event) => event.kind === 'fingerprint_changed'
|
||||
).length,
|
||||
reportAcceptedCount: recentEvents.filter((event) => event.kind === 'report_accepted')
|
||||
.length,
|
||||
reportRejectedCount: recentEvents.filter((event) => event.kind === 'report_rejected')
|
||||
fingerprintChangeCount: recentEvents.filter((event) => event.kind === 'fingerprint_changed')
|
||||
.length,
|
||||
reportAcceptedCount: recentEvents.filter((event) => event.kind === 'report_accepted').length,
|
||||
reportRejectedCount: recentEvents.filter((event) => event.kind === 'report_rejected').length,
|
||||
recentEvents,
|
||||
};
|
||||
return {
|
||||
|
|
@ -349,7 +347,7 @@ export class JsonMemberWorkSyncStore
|
|||
await withFileLock(this.paths.getPendingReportsPath(teamName), async () => {
|
||||
const existing = await this.readPendingFile(teamName);
|
||||
const current = existing.intents[id];
|
||||
if (!current || current.status !== 'pending') {
|
||||
if (current?.status !== 'pending') {
|
||||
return;
|
||||
}
|
||||
existing.intents[id] = {
|
||||
|
|
@ -471,7 +469,7 @@ export class JsonMemberWorkSyncStore
|
|||
|
||||
async markDelivered(input: MemberWorkSyncOutboxMarkDeliveredInput): Promise<void> {
|
||||
await this.updateOutboxItem(input.teamName, input.id, (current) => {
|
||||
if (!current || current.attemptGeneration !== input.attemptGeneration) {
|
||||
if (current?.attemptGeneration !== input.attemptGeneration) {
|
||||
return current;
|
||||
}
|
||||
const next: MemberWorkSyncOutboxItem = {
|
||||
|
|
@ -501,7 +499,7 @@ export class JsonMemberWorkSyncStore
|
|||
|
||||
async markFailed(input: MemberWorkSyncOutboxMarkFailedInput): Promise<void> {
|
||||
await this.updateOutboxItem(input.teamName, input.id, (current) => {
|
||||
if (!current || current.attemptGeneration !== input.attemptGeneration) {
|
||||
if (current?.attemptGeneration !== input.attemptGeneration) {
|
||||
return current;
|
||||
}
|
||||
const next: MemberWorkSyncOutboxItem = {
|
||||
|
|
@ -589,9 +587,7 @@ export class JsonMemberWorkSyncStore
|
|||
private async updateOutboxItem(
|
||||
teamName: string,
|
||||
id: string,
|
||||
updater: (
|
||||
current: MemberWorkSyncOutboxItem | undefined
|
||||
) => MemberWorkSyncOutboxItem | undefined
|
||||
updater: (current: MemberWorkSyncOutboxItem | undefined) => MemberWorkSyncOutboxItem | undefined
|
||||
): Promise<void> {
|
||||
await this.enqueue(teamName, async () => {
|
||||
await withFileLock(this.paths.getOutboxPath(teamName), async () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { MemberWorkSyncReconcileContext } from '../../core/application/MemberWorkSyncReconciler';
|
||||
import type { MemberWorkSyncLoggerPort } from '../../core/application';
|
||||
import type { MemberWorkSyncReconcileContext } from '../../core/application/MemberWorkSyncReconciler';
|
||||
|
||||
export type MemberWorkSyncTriggerReason =
|
||||
| 'startup_scan'
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import type { MemberWorkSyncBusySignalPort } from '../../core/application';
|
||||
|
||||
import type { TeamChangeEvent, ToolActivityEventPayload } from '@shared/types';
|
||||
|
||||
const DEFAULT_TOOL_ACTIVITY_BUSY_GRACE_MS = 90_000;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import fs from 'fs/promises';
|
||||
|
||||
import { RuntimeTurnSettledSpoolPaths } from './RuntimeTurnSettledSpoolPaths';
|
||||
import type { RuntimeTurnSettledSpoolPaths } from './RuntimeTurnSettledSpoolPaths';
|
||||
|
||||
const HOOK_SCRIPT_CONTENT = `#!/bin/sh
|
||||
set +e
|
||||
|
|
@ -58,6 +58,7 @@ export class ShellRuntimeTurnSettledHookScriptInstaller {
|
|||
|
||||
const scriptPath = this.paths.getHookScriptPath();
|
||||
await fs.writeFile(scriptPath, HOOK_SCRIPT_CONTENT, 'utf8');
|
||||
// eslint-disable-next-line sonarjs/file-permissions -- the hook script must be executable on POSIX hosts.
|
||||
await fs.chmod(scriptPath, 0o755);
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@ import { useEffect, useState } from 'react';
|
|||
|
||||
import { api } from '@renderer/api';
|
||||
|
||||
import type { MemberWorkSyncStatus } from '../../contracts';
|
||||
import {
|
||||
toMemberWorkSyncStatusViewModel,
|
||||
type MemberWorkSyncStatusViewModel,
|
||||
toMemberWorkSyncStatusViewModel,
|
||||
} from '../adapters/memberWorkSyncStatusViewModel';
|
||||
|
||||
import type { MemberWorkSyncStatus } from '../../contracts';
|
||||
|
||||
export interface UseMemberWorkSyncStatusOptions {
|
||||
teamName?: string | null;
|
||||
memberName?: string | null;
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
import { Badge } from '@renderer/components/ui/badge';
|
||||
import { cn } from '@renderer/lib/utils';
|
||||
import type React from 'react';
|
||||
|
||||
import type { MemberWorkSyncStatus } from '../../contracts';
|
||||
import {
|
||||
toMemberWorkSyncStatusViewModel,
|
||||
type MemberWorkSyncStatusViewModel,
|
||||
toMemberWorkSyncStatusViewModel,
|
||||
} from '../adapters/memberWorkSyncStatusViewModel';
|
||||
|
||||
interface MemberWorkSyncBadgeProps {
|
||||
import type { MemberWorkSyncStatus } from '../../contracts';
|
||||
import type React from 'react';
|
||||
|
||||
type MemberWorkSyncBadgeProps = Readonly<{
|
||||
status?: MemberWorkSyncStatus | null;
|
||||
viewModel?: MemberWorkSyncStatusViewModel;
|
||||
className?: string;
|
||||
}
|
||||
}>;
|
||||
|
||||
const toneClassName: Record<MemberWorkSyncStatusViewModel['tone'], string> = {
|
||||
neutral: 'border-[var(--color-border)] text-[var(--color-text-muted)]',
|
||||
|
|
@ -32,7 +33,11 @@ export function MemberWorkSyncBadge({
|
|||
return (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn('cursor-default whitespace-nowrap font-medium', toneClassName[resolved.tone], className)}
|
||||
className={cn(
|
||||
'cursor-default whitespace-nowrap font-medium',
|
||||
toneClassName[resolved.tone],
|
||||
className
|
||||
)}
|
||||
title={resolved.tooltip}
|
||||
>
|
||||
{resolved.label}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import { MemberWorkSyncBadge } from './MemberWorkSyncBadge';
|
||||
import type { MemberWorkSyncStatus } from '../../contracts';
|
||||
import { toMemberWorkSyncStatusViewModel } from '../adapters/memberWorkSyncStatusViewModel';
|
||||
|
||||
import { MemberWorkSyncBadge } from './MemberWorkSyncBadge';
|
||||
|
||||
import type { MemberWorkSyncStatus } from '../../contracts';
|
||||
import type React from 'react';
|
||||
|
||||
interface MemberWorkSyncDetailsProps {
|
||||
type MemberWorkSyncDetailsProps = Readonly<{
|
||||
status: MemberWorkSyncStatus | null;
|
||||
showDiagnostics?: boolean;
|
||||
}
|
||||
}>;
|
||||
|
||||
function shortFingerprint(fingerprint?: string): string {
|
||||
if (!fingerprint) {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
import type React from 'react';
|
||||
import { useMemberWorkSyncStatus } from '../hooks/useMemberWorkSyncStatus';
|
||||
|
||||
import { MemberWorkSyncBadge } from './MemberWorkSyncBadge';
|
||||
import { MemberWorkSyncDetails } from './MemberWorkSyncDetails';
|
||||
import { useMemberWorkSyncStatus } from '../hooks/useMemberWorkSyncStatus';
|
||||
|
||||
interface MemberWorkSyncStatusPanelProps {
|
||||
import type React from 'react';
|
||||
|
||||
type MemberWorkSyncStatusPanelProps = Readonly<{
|
||||
teamName: string;
|
||||
memberName: string;
|
||||
enabled?: boolean;
|
||||
showDiagnostics?: boolean;
|
||||
}
|
||||
}>;
|
||||
|
||||
export function MemberWorkSyncStatusPanel({
|
||||
teamName,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import {
|
|||
type RecentProjectsFeatureFacade,
|
||||
registerRecentProjectsHttp,
|
||||
} from '@features/recent-projects/main';
|
||||
import type { MemberWorkSyncFeatureFacade } from '@features/member-work-sync/main';
|
||||
import { createLogger } from '@shared/utils/logger';
|
||||
|
||||
import { registerConfigRoutes } from './config';
|
||||
|
|
@ -36,6 +35,7 @@ import type {
|
|||
import type { SshConnectionManager } from '../services/infrastructure/SshConnectionManager';
|
||||
import type { TeamDataService } from '../services/team/TeamDataService';
|
||||
import type { TeamProvisioningService } from '../services/team/TeamProvisioningService';
|
||||
import type { MemberWorkSyncFeatureFacade } from '@features/member-work-sync/main';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
|
||||
const logger = createLogger('HTTP:routes');
|
||||
|
|
|
|||
|
|
@ -30,18 +30,18 @@ import {
|
|||
type CodexModelCatalogFeatureFacade,
|
||||
createCodexModelCatalogFeature,
|
||||
} from '@features/codex-model-catalog/main';
|
||||
import {
|
||||
createRecentProjectsFeature,
|
||||
type RecentProjectsFeatureFacade,
|
||||
registerRecentProjectsIpc,
|
||||
removeRecentProjectsIpc,
|
||||
} from '@features/recent-projects/main';
|
||||
import {
|
||||
createMemberWorkSyncFeature,
|
||||
type MemberWorkSyncFeatureFacade,
|
||||
registerMemberWorkSyncIpc,
|
||||
removeMemberWorkSyncIpc,
|
||||
} from '@features/member-work-sync/main';
|
||||
import {
|
||||
createRecentProjectsFeature,
|
||||
type RecentProjectsFeatureFacade,
|
||||
registerRecentProjectsIpc,
|
||||
removeRecentProjectsIpc,
|
||||
} from '@features/recent-projects/main';
|
||||
import {
|
||||
createRuntimeProviderManagementFeature,
|
||||
registerRuntimeProviderManagementIpc,
|
||||
|
|
@ -179,19 +179,19 @@ import {
|
|||
SshConnectionManager,
|
||||
TaskBoundaryParser,
|
||||
TeamDataService,
|
||||
TeamKanbanManager,
|
||||
TeamLogSourceTracker,
|
||||
TeammateToolTracker,
|
||||
TeamMemberLogsFinder,
|
||||
TeamMembersMetaStore,
|
||||
TeamProvisioningService,
|
||||
TeamRuntimeAdapterRegistry,
|
||||
TeamKanbanManager,
|
||||
TeamMembersMetaStore,
|
||||
TeamTaskReader,
|
||||
TeamTaskStallJournal,
|
||||
TeamTaskStallMonitor,
|
||||
TeamTaskStallNotifier,
|
||||
TeamTaskStallPolicy,
|
||||
TeamTaskStallSnapshotSource,
|
||||
TeamTaskReader,
|
||||
UpdaterService,
|
||||
} from './services';
|
||||
|
||||
|
|
|
|||
|
|
@ -1152,7 +1152,7 @@ export class TaskChangeLedgerReader {
|
|||
string,
|
||||
{ event: LedgerEvent; index: number; rank: number }
|
||||
>();
|
||||
const passthrough: Array<{ event: LedgerEvent; index: number }> = [];
|
||||
const passthrough: { event: LedgerEvent; index: number }[] = [];
|
||||
|
||||
events.forEach((event, index) => {
|
||||
const sourceImportKey = this.sourceImportKeyForEvent(event);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import {
|
|||
resolveAnthropicFastMode,
|
||||
resolveAnthropicRuntimeSelection,
|
||||
} from '@features/anthropic-runtime-profile/main';
|
||||
import type { RuntimeTurnSettledProvider } from '@features/member-work-sync/core/domain';
|
||||
import {
|
||||
buildCodexFastModeArgs,
|
||||
resolveCodexFastMode,
|
||||
|
|
@ -239,6 +238,7 @@ import type {
|
|||
TeamRuntimePrepareResult,
|
||||
TeamRuntimeStopInput,
|
||||
} from './runtime';
|
||||
import type { RuntimeTurnSettledProvider } from '@features/member-work-sync/main';
|
||||
|
||||
/**
|
||||
* Kill a team CLI process using SIGKILL (uncatchable).
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { MemberWorkSyncStatusPanel } from '@features/member-work-sync/renderer';
|
||||
import { Button } from '@renderer/components/ui/button';
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader } from '@renderer/components/ui/dialog';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@renderer/components/ui/tabs';
|
||||
import { useMemberStats } from '@renderer/hooks/useMemberStats';
|
||||
import { useStore } from '@renderer/store';
|
||||
import { selectMemberMessagesForTeamMember } from '@renderer/store/slices/teamSlice';
|
||||
import { MemberWorkSyncStatusPanel } from '@features/member-work-sync/renderer';
|
||||
import {
|
||||
buildMemberLaunchDiagnosticsPayload,
|
||||
getMemberLaunchDiagnosticsErrorMessage,
|
||||
|
|
|
|||
|
|
@ -95,8 +95,6 @@ import type { TerminalAPI } from './terminal';
|
|||
import type { TmuxAPI } from './tmux';
|
||||
import type { WaterfallData } from './visualization';
|
||||
import type { CodexAccountElectronApi } from '@features/codex-account/contracts';
|
||||
import type { RecentProjectsElectronApi } from '@features/recent-projects/contracts';
|
||||
import type { RuntimeProviderManagementApi } from '@features/runtime-provider-management/contracts';
|
||||
import type {
|
||||
MemberWorkSyncMetricsRequest,
|
||||
MemberWorkSyncReportRequest,
|
||||
|
|
@ -105,6 +103,8 @@ import type {
|
|||
MemberWorkSyncStatusRequest,
|
||||
MemberWorkSyncTeamMetrics,
|
||||
} from '@features/member-work-sync/contracts';
|
||||
import type { RecentProjectsElectronApi } from '@features/recent-projects/contracts';
|
||||
import type { RuntimeProviderManagementApi } from '@features/runtime-provider-management/contracts';
|
||||
import type {
|
||||
ConversationGroup,
|
||||
FileChangeEvent,
|
||||
|
|
|
|||
|
|
@ -79,8 +79,10 @@ describe('runtime turn-settled hook settings', () => {
|
|||
expect(fs.existsSync(paths.getProcessedDir())).toBe(true);
|
||||
expect(fs.existsSync(paths.getInvalidDir())).toBe(true);
|
||||
|
||||
const stat = fs.statSync(result.scriptPath);
|
||||
expect(stat.mode & 0o111).toBeGreaterThan(0);
|
||||
if (process.platform !== 'win32') {
|
||||
const stat = fs.statSync(result.scriptPath);
|
||||
expect(stat.mode & 0o111).toBeGreaterThan(0);
|
||||
}
|
||||
expect(fs.readFileSync(result.scriptPath, 'utf8')).toContain('dd bs="$max_bytes"');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue