@taskforcehq/taskforce 0.3.301 → 0.3.303
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Taskforce.module.css +37 -2
- package/dist/TaskforceCore.filter-persistence.test.js +1 -1
- package/dist/TaskforceCore.js +72 -33
- package/dist/TaskforceCore.test.js +848 -130
- package/dist/components/features/TaskSettings.js +14 -82
- package/dist/components/features/TaskSettings.test.js +33 -70
- package/dist/components/features/settings/TaxonomyLibraryManager.js +8 -2
- package/dist/components/features/settings/TaxonomyLibraryManager.test.js +1 -1
- package/dist/components/task/TaskActionHeader.js +6 -3
- package/dist/components/task/TaskCard.js +6 -3
- package/dist/components/task/TaskForm.js +2 -2
- package/dist/components/task/TaskForm.test.js +41 -2
- package/dist/components/task/TaskKanban.test.js +17 -0
- package/dist/components/ui/ReferenceBadgeButton.js +1 -1
- package/dist/components/views/PlansPage.d.ts +3 -1
- package/dist/components/views/PlansPage.js +109 -49
- package/dist/components/views/PlansPage.test.d.ts +1 -0
- package/dist/components/views/PlansPage.test.js +100 -0
- package/dist/components/views/StandaloneLayout.d.ts +1 -0
- package/dist/components/views/StandaloneLayout.js +473 -256
- package/dist/components/views/panels/PlanningDrawer.js +18 -1
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +48 -0
- package/dist/components/views/standalone/modals/AccountHubModal.js +36 -0
- package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
- package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.js +8 -0
- package/dist/components/views/standalone/modals/EditProfileModal.d.ts +26 -0
- package/dist/components/views/standalone/modals/EditProfileModal.js +10 -0
- package/dist/components/views/standalone/modals/HelpModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/HelpModal.js +8 -0
- package/dist/components/views/standalone/modals/ScheduleWarningModal.d.ts +13 -0
- package/dist/components/views/standalone/modals/ScheduleWarningModal.js +6 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +61 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.js +38 -0
- package/dist/components/views/standalone/modals/TeamManagementModal.d.ts +64 -0
- package/dist/components/views/standalone/modals/TeamManagementModal.js +10 -0
- package/dist/components/views/standalone/modals/types.d.ts +2 -0
- package/dist/components/views/standalone/modals/types.js +1 -0
- package/dist/components/views/teamManagementAccess.d.ts +1 -1
- package/dist/components/views/teamManagementAccess.js +2 -2
- package/dist/components/views/teamManagementAccess.test.js +3 -3
- package/dist/config/deployment.js +2 -2
- package/dist/config/deployment.test.js +5 -3
- package/dist/core/AiProfileService.js +7 -2
- package/dist/core/AiProfiles.test.js +19 -0
- package/dist/core/EntitlementsPolicy.test.js +2 -2
- package/dist/core/GlobalSettingsService.js +78 -18
- package/dist/core/PlanEntitlementService.d.ts +29 -5
- package/dist/core/PlanEntitlementService.js +297 -219
- package/dist/core/SignupMonetizationSettings.test.js +173 -49
- package/dist/core/SyncReconciliationService.d.ts +2 -0
- package/dist/core/SyncReconciliationService.js +3 -0
- package/dist/core/SystemAdmin.test.js +186 -96
- package/dist/core/TaskAuditTimeline.test.js +40 -0
- package/dist/core/Taskforce.d.ts +100 -10
- package/dist/core/Taskforce.ids.test.js +133 -0
- package/dist/core/Taskforce.js +459 -127
- package/dist/core/Taskforce.mcpAuth.test.js +0 -2
- package/dist/core/UserProfileAvatarDrafts.test.d.ts +1 -0
- package/dist/core/UserProfileAvatarDrafts.test.js +111 -0
- package/dist/core/taskReferencePolicy.d.ts +28 -0
- package/dist/core/taskReferencePolicy.js +189 -0
- package/dist/core/types.d.ts +13 -12
- package/dist/hooks/tasks/useTaskFormActions.js +3 -0
- package/dist/hooks/useSyncOrchestrator.js +17 -3
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +97 -5
- package/dist/hooks/useTaskData.js +3 -2
- package/dist/hooks/useTaskData.test.js +66 -0
- package/dist/hooks/useTaskforce.d.ts +8 -0
- package/dist/hooks/useTaskforce.js +72 -19
- package/dist/hooks/useTaskforce.sync-behavior.test.js +185 -6
- package/dist/mcp/aiProfileBootstrap.d.ts +29 -0
- package/dist/mcp/aiProfileBootstrap.js +80 -0
- package/dist/mcp/aiProfileBootstrap.test.d.ts +1 -0
- package/dist/mcp/aiProfileBootstrap.test.js +68 -0
- package/dist/mcp/clientIntegrations.d.ts +2 -8
- package/dist/mcp/clientIntegrations.js +21 -40
- package/dist/mcp/clientIntegrations.test.js +28 -2
- package/dist/mcp/clientProfileDefaults.d.ts +12 -0
- package/dist/mcp/clientProfileDefaults.js +37 -0
- package/dist/mcp/runtime.d.ts +10 -7
- package/dist/mcp/runtime.js +131 -48
- package/dist/mcp/runtime.test.js +321 -83
- package/dist/migrations/billingSchemaParity.test.js +3 -0
- package/dist/migrations/taskSchemaMigrations.js +68 -1
- package/dist/qaOpen.test.d.ts +1 -0
- package/dist/qaOpen.test.js +56 -0
- package/dist/server/index.cookieProxy.test.js +2 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +17 -1
- package/dist/server/index.test.js +15 -1
- package/dist/server/mockStripe.d.ts +1 -0
- package/dist/server/mockStripe.js +2 -0
- package/dist/server/routes/admin.js +105 -45
- package/dist/server/routes/auth.d.ts +6 -0
- package/dist/server/routes/auth.js +505 -30
- package/dist/server/routes/billing.js +517 -24
- package/dist/server/routes/billing.test.js +967 -45
- package/dist/server/routes/documents.js +6 -1
- package/dist/server/routes/shared.d.ts +1 -1
- package/dist/server/routes/shared.js +16 -6
- package/dist/server/routes/sync.integration.test.js +290 -0
- package/dist/server/routes/sync.js +234 -1922
- package/dist/server/routes/tasks.js +18 -4
- package/dist/server/routes.js +29 -22
- package/dist/server/routes.test.js +639 -82
- package/dist/shared/runtimeContract.d.ts +1 -1
- package/dist/sync/cloudSyncApi.d.ts +1 -0
- package/dist/sync/collaborationSyncPayload.d.ts +17 -0
- package/dist/sync/collaborationSyncPayload.js +131 -0
- package/dist/sync/collaborationSyncPayload.test.d.ts +1 -0
- package/dist/sync/collaborationSyncPayload.test.js +104 -0
- package/dist/sync/collaborationSyncState.d.ts +15 -0
- package/dist/sync/collaborationSyncState.js +60 -0
- package/dist/sync/contentSyncPayload.d.ts +45 -0
- package/dist/sync/contentSyncPayload.js +104 -0
- package/dist/sync/contentSyncPayload.test.d.ts +1 -0
- package/dist/sync/contentSyncPayload.test.js +104 -0
- package/dist/sync/contentSyncState.d.ts +98 -0
- package/dist/sync/contentSyncState.js +1004 -0
- package/dist/sync/planningSyncPayload.d.ts +9 -0
- package/dist/sync/planningSyncPayload.js +82 -0
- package/dist/sync/planningSyncPayload.test.d.ts +1 -0
- package/dist/sync/planningSyncPayload.test.js +68 -0
- package/dist/sync/syncApplyHandlers.d.ts +83 -0
- package/dist/sync/syncApplyHandlers.js +445 -0
- package/dist/sync/syncService.d.ts +1 -0
- package/dist/sync/syncService.js +21 -8
- package/dist/sync/taskSyncPayload.d.ts +33 -0
- package/dist/sync/taskSyncPayload.js +126 -0
- package/dist/sync/taskSyncPayload.test.d.ts +1 -0
- package/dist/sync/taskSyncPayload.test.js +54 -0
- package/dist/sync/workspacePullFeed.d.ts +38 -0
- package/dist/sync/workspacePullFeed.js +349 -0
- package/dist/sync/workspacePullFeed.test.d.ts +1 -0
- package/dist/sync/workspacePullFeed.test.js +278 -0
- package/dist/sync/workspaceSyncModel.d.ts +4 -1
- package/dist/sync/workspaceSyncModel.js +9 -2
- package/dist/sync/workspaceSyncModel.test.js +29 -0
- package/dist/test/setup.js +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/ui/assets/{AgentsModule-Bdq3T1Ts.js → AgentsModule-CpsTmrIW.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-jKUVo-PJ.js → AnnotatedAttachmentWorkspace-C_TmXZwA.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-DuQa-uIC.js → DocumentWorkspace-C3GyzrWm.js} +2 -2
- package/dist/ui/assets/{InitiativesModule-C8Pi2VTk.js → InitiativesModule-4-Rh2K2n.js} +1 -1
- package/dist/ui/assets/PlansPage-BP7AYOqr.js +1 -0
- package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +8 -0
- package/dist/ui/assets/{TaskSettings-BwB9NtmJ.css → TaskSettings-CnIBL_Eb.css} +1 -1
- package/dist/ui/assets/{WorkflowsModule-b6TLuHwJ.js → WorkflowsModule-CGk9s3NJ.js} +1 -1
- package/dist/ui/assets/index-CLIMgR6g.js +6 -0
- package/dist/ui/assets/index-DneETxcu.css +1 -0
- package/dist/ui/assets/{vendor-icons-QZyhEwgT.js → vendor-icons-pWocEipT.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
- package/dist/utils/accountProfileSummaryCache.js +97 -0
- package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
- package/dist/utils/accountProfileSummaryCache.test.js +63 -0
- package/dist/utils/taskActivity.js +17 -4
- package/dist/utils/taskActivity.test.js +27 -0
- package/dist/utils/taskNormalization.js +10 -2
- package/dist/utils/taskReferences.d.ts +11 -1
- package/dist/utils/taskReferences.js +33 -1
- package/package.json +4 -3
- package/scripts/qa-open-lib.mjs +175 -0
- package/scripts/qa-open-lib.mjs.d.ts +49 -0
- package/scripts/qa-open.mjs +352 -0
- package/dist/ui/assets/PlansPage-B6BDECSi.js +0 -1
- package/dist/ui/assets/TaskSettings-DUJgRTPU.js +0 -8
- package/dist/ui/assets/index-BiiF1cLQ.css +0 -1
- package/dist/ui/assets/index-xBWLPOEb.js +0 -6
package/dist/ui/index.html
CHANGED
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png" />
|
|
9
9
|
<link rel="apple-touch-icon" href="/taskforce/favicon/apple-touch-icon.png" />
|
|
10
10
|
<title>Taskforce Dashboard</title>
|
|
11
|
-
<script type="module" crossorigin src="/taskforce/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/taskforce/assets/index-CLIMgR6g.js"></script>
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/taskforce/assets/vendor-react-CKJs5o3c.js">
|
|
13
|
-
<link rel="modulepreload" crossorigin href="/taskforce/assets/vendor-icons-
|
|
13
|
+
<link rel="modulepreload" crossorigin href="/taskforce/assets/vendor-icons-pWocEipT.js">
|
|
14
14
|
<link rel="modulepreload" crossorigin href="/taskforce/assets/vendor-markdown-BUxTU7dS.js">
|
|
15
15
|
<link rel="modulepreload" crossorigin href="/taskforce/assets/vendor-dnd-DRzYolkg.js">
|
|
16
16
|
<link rel="modulepreload" crossorigin href="/taskforce/assets/vendor-router-BbWMxlnO.js">
|
|
17
|
-
<link rel="stylesheet" crossorigin href="/taskforce/assets/index-
|
|
17
|
+
<link rel="stylesheet" crossorigin href="/taskforce/assets/index-DneETxcu.css">
|
|
18
18
|
</head>
|
|
19
19
|
<body>
|
|
20
20
|
<div id="root"></div>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export declare const ACCOUNT_PROFILE_SUMMARY_PATH = "/api/taskforce/account/profile-summary";
|
|
2
|
+
export type AccountProfileSummary = {
|
|
3
|
+
stripeStatus?: string | null;
|
|
4
|
+
stripeCustomerId?: string | null;
|
|
5
|
+
stripeSubscriptionId?: string | null;
|
|
6
|
+
stripePriceId?: string | null;
|
|
7
|
+
billingInterval?: 'month' | 'year' | null;
|
|
8
|
+
trialEnd?: string | null;
|
|
9
|
+
currentPeriodEnd?: string | null;
|
|
10
|
+
effectiveUntil?: string | null;
|
|
11
|
+
entitlementState?: string;
|
|
12
|
+
planVersionId?: string;
|
|
13
|
+
planId?: string;
|
|
14
|
+
planName?: string | null;
|
|
15
|
+
gate?: string | null;
|
|
16
|
+
message?: string | null;
|
|
17
|
+
billingConflictCode?: 'CHECKOUT_SESSION_EXPIRED' | 'CHECKOUT_SESSION_SUPERSEDED' | null;
|
|
18
|
+
planSelectionRequired?: boolean;
|
|
19
|
+
workspaceId?: string | null;
|
|
20
|
+
workspaceRole?: 'owner' | 'admin' | 'member' | 'read-only' | null;
|
|
21
|
+
canManageWorkspace?: boolean;
|
|
22
|
+
teamPlanMode?: 'personal' | 'team' | 'unknown';
|
|
23
|
+
teamManagementAllowed?: boolean;
|
|
24
|
+
};
|
|
25
|
+
export declare function invalidateAccountProfileSummaryCache(rawUrl: string, options?: {
|
|
26
|
+
identityKey?: string | null;
|
|
27
|
+
}): void;
|
|
28
|
+
export declare function loadAccountProfileSummaryCached(rawUrl: string, options?: {
|
|
29
|
+
force?: boolean;
|
|
30
|
+
identityKey?: string | null;
|
|
31
|
+
}): Promise<AccountProfileSummary | null>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { logBootstrapDebug } from '../hooks/workspace/workspaceLocalState';
|
|
2
|
+
export const ACCOUNT_PROFILE_SUMMARY_PATH = '/api/taskforce/account/profile-summary';
|
|
3
|
+
const ACCOUNT_PROFILE_SUMMARY_CACHE_TTL_MS = 90_000;
|
|
4
|
+
const accountProfileSummaryCache = new Map();
|
|
5
|
+
function getTimingNow() {
|
|
6
|
+
return typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
7
|
+
}
|
|
8
|
+
function normalizeAccountProfileSummaryCacheKey(rawUrl, identityKey) {
|
|
9
|
+
const url = String(rawUrl || '').trim();
|
|
10
|
+
if (!url)
|
|
11
|
+
return '';
|
|
12
|
+
const normalizedIdentityKey = String(identityKey || '').trim();
|
|
13
|
+
return normalizedIdentityKey ? `${url}::${normalizedIdentityKey}` : url;
|
|
14
|
+
}
|
|
15
|
+
export function invalidateAccountProfileSummaryCache(rawUrl, options) {
|
|
16
|
+
accountProfileSummaryCache.delete(normalizeAccountProfileSummaryCacheKey(rawUrl, options?.identityKey));
|
|
17
|
+
}
|
|
18
|
+
export async function loadAccountProfileSummaryCached(rawUrl, options) {
|
|
19
|
+
const url = String(rawUrl || '').trim();
|
|
20
|
+
const cacheKey = normalizeAccountProfileSummaryCacheKey(rawUrl, options?.identityKey);
|
|
21
|
+
if (!cacheKey || !url)
|
|
22
|
+
return null;
|
|
23
|
+
const force = options?.force === true;
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
const cached = accountProfileSummaryCache.get(cacheKey);
|
|
26
|
+
if (!force && cached?.payload !== undefined && (now - cached.fetchedAt) < ACCOUNT_PROFILE_SUMMARY_CACHE_TTL_MS) {
|
|
27
|
+
logBootstrapDebug('account_profile_summary_cache_hit', {
|
|
28
|
+
url,
|
|
29
|
+
cacheKey,
|
|
30
|
+
ageMs: now - cached.fetchedAt
|
|
31
|
+
});
|
|
32
|
+
return cached.payload;
|
|
33
|
+
}
|
|
34
|
+
if (!force && cached?.promise) {
|
|
35
|
+
logBootstrapDebug('account_profile_summary_request_reused', {
|
|
36
|
+
url,
|
|
37
|
+
cacheKey
|
|
38
|
+
});
|
|
39
|
+
return cached.promise;
|
|
40
|
+
}
|
|
41
|
+
const startedAt = getTimingNow();
|
|
42
|
+
const request = (async () => {
|
|
43
|
+
const res = await fetch(url, {
|
|
44
|
+
method: 'GET',
|
|
45
|
+
credentials: 'include'
|
|
46
|
+
});
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
throw new Error(String((await res.json().catch(() => ({})))?.error || 'Failed to load account summary.'));
|
|
49
|
+
}
|
|
50
|
+
const payload = await res.json().catch(() => ({}));
|
|
51
|
+
const normalized = (payload || null);
|
|
52
|
+
accountProfileSummaryCache.set(cacheKey, {
|
|
53
|
+
payload: normalized,
|
|
54
|
+
fetchedAt: Date.now(),
|
|
55
|
+
promise: null
|
|
56
|
+
});
|
|
57
|
+
logBootstrapDebug('account_profile_summary_loaded', {
|
|
58
|
+
url,
|
|
59
|
+
cacheKey,
|
|
60
|
+
durationMs: Math.round(getTimingNow() - startedAt),
|
|
61
|
+
fromCache: false
|
|
62
|
+
});
|
|
63
|
+
return normalized;
|
|
64
|
+
})().catch((error) => {
|
|
65
|
+
const stalePayload = cached?.payload ?? null;
|
|
66
|
+
if (stalePayload) {
|
|
67
|
+
accountProfileSummaryCache.set(cacheKey, {
|
|
68
|
+
payload: stalePayload,
|
|
69
|
+
fetchedAt: cached?.fetchedAt ?? Date.now(),
|
|
70
|
+
promise: null
|
|
71
|
+
});
|
|
72
|
+
logBootstrapDebug('account_profile_summary_failed', {
|
|
73
|
+
url,
|
|
74
|
+
cacheKey,
|
|
75
|
+
durationMs: Math.round(getTimingNow() - startedAt),
|
|
76
|
+
error: error instanceof Error ? error.message : String(error || 'Unknown error'),
|
|
77
|
+
returnedStale: true
|
|
78
|
+
});
|
|
79
|
+
return stalePayload;
|
|
80
|
+
}
|
|
81
|
+
accountProfileSummaryCache.delete(cacheKey);
|
|
82
|
+
logBootstrapDebug('account_profile_summary_failed', {
|
|
83
|
+
url,
|
|
84
|
+
cacheKey,
|
|
85
|
+
durationMs: Math.round(getTimingNow() - startedAt),
|
|
86
|
+
error: error instanceof Error ? error.message : String(error || 'Unknown error'),
|
|
87
|
+
returnedStale: false
|
|
88
|
+
});
|
|
89
|
+
throw error;
|
|
90
|
+
});
|
|
91
|
+
accountProfileSummaryCache.set(cacheKey, {
|
|
92
|
+
payload: cached?.payload ?? null,
|
|
93
|
+
fetchedAt: cached?.fetchedAt ?? 0,
|
|
94
|
+
promise: request
|
|
95
|
+
});
|
|
96
|
+
return request;
|
|
97
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { ACCOUNT_PROFILE_SUMMARY_PATH, invalidateAccountProfileSummaryCache, loadAccountProfileSummaryCached } from './accountProfileSummaryCache';
|
|
3
|
+
describe('accountProfileSummaryCache', () => {
|
|
4
|
+
afterEach(() => {
|
|
5
|
+
vi.restoreAllMocks();
|
|
6
|
+
vi.useRealTimers();
|
|
7
|
+
invalidateAccountProfileSummaryCache(ACCOUNT_PROFILE_SUMMARY_PATH, { identityKey: 'user-a' });
|
|
8
|
+
invalidateAccountProfileSummaryCache(ACCOUNT_PROFILE_SUMMARY_PATH, { identityKey: 'user-b' });
|
|
9
|
+
});
|
|
10
|
+
it('keeps account summaries isolated per authenticated user', async () => {
|
|
11
|
+
const fetchMock = vi.fn()
|
|
12
|
+
.mockResolvedValueOnce({
|
|
13
|
+
ok: true,
|
|
14
|
+
json: async () => ({ planId: 'team-5', planVersionId: 'team-5-v1' })
|
|
15
|
+
})
|
|
16
|
+
.mockResolvedValueOnce({
|
|
17
|
+
ok: true,
|
|
18
|
+
json: async () => ({ planId: 'starter', planVersionId: 'starter-v1' })
|
|
19
|
+
});
|
|
20
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
21
|
+
const first = await loadAccountProfileSummaryCached(ACCOUNT_PROFILE_SUMMARY_PATH, { identityKey: 'user-a' });
|
|
22
|
+
const second = await loadAccountProfileSummaryCached(ACCOUNT_PROFILE_SUMMARY_PATH, { identityKey: 'user-b' });
|
|
23
|
+
expect(first?.planId).toBe('team-5');
|
|
24
|
+
expect(second?.planId).toBe('starter');
|
|
25
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
26
|
+
expect(fetchMock).toHaveBeenNthCalledWith(1, ACCOUNT_PROFILE_SUMMARY_PATH, expect.any(Object));
|
|
27
|
+
expect(fetchMock).toHaveBeenNthCalledWith(2, ACCOUNT_PROFILE_SUMMARY_PATH, expect.any(Object));
|
|
28
|
+
});
|
|
29
|
+
it('reuses the cached summary inside the cache window', async () => {
|
|
30
|
+
vi.useFakeTimers();
|
|
31
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
32
|
+
ok: true,
|
|
33
|
+
json: async () => ({ planId: 'team-5', planVersionId: 'team-5-v1' })
|
|
34
|
+
});
|
|
35
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
36
|
+
const first = await loadAccountProfileSummaryCached(ACCOUNT_PROFILE_SUMMARY_PATH, { identityKey: 'user-a' });
|
|
37
|
+
vi.advanceTimersByTime(30_000);
|
|
38
|
+
const second = await loadAccountProfileSummaryCached(ACCOUNT_PROFILE_SUMMARY_PATH, { identityKey: 'user-a' });
|
|
39
|
+
expect(first?.planId).toBe('team-5');
|
|
40
|
+
expect(second?.planId).toBe('team-5');
|
|
41
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
42
|
+
});
|
|
43
|
+
it('returns the last successful summary when a forced refresh fails', async () => {
|
|
44
|
+
const fetchMock = vi.fn()
|
|
45
|
+
.mockResolvedValueOnce({
|
|
46
|
+
ok: true,
|
|
47
|
+
json: async () => ({ planId: 'team-5', planVersionId: 'team-5-v1', teamManagementAllowed: true })
|
|
48
|
+
})
|
|
49
|
+
.mockResolvedValueOnce({
|
|
50
|
+
ok: false,
|
|
51
|
+
json: async () => ({ error: 'Stripe unavailable' })
|
|
52
|
+
});
|
|
53
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
54
|
+
const warm = await loadAccountProfileSummaryCached(ACCOUNT_PROFILE_SUMMARY_PATH, { identityKey: 'user-a' });
|
|
55
|
+
const refreshed = await loadAccountProfileSummaryCached(ACCOUNT_PROFILE_SUMMARY_PATH, {
|
|
56
|
+
identityKey: 'user-a',
|
|
57
|
+
force: true
|
|
58
|
+
});
|
|
59
|
+
expect(warm?.planId).toBe('team-5');
|
|
60
|
+
expect(refreshed).toEqual(warm);
|
|
61
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
function getActivityFieldLabel(field) {
|
|
2
|
+
if (field === 'title')
|
|
3
|
+
return 'TITLE';
|
|
4
|
+
if (field === 'description')
|
|
5
|
+
return 'DESCRIPTION';
|
|
6
|
+
if (field === 'assignee')
|
|
7
|
+
return 'ASSIGNED TO';
|
|
8
|
+
return field;
|
|
9
|
+
}
|
|
1
10
|
function formatValue(value, field, formatter) {
|
|
2
11
|
const formatted = formatter?.(field, value);
|
|
3
12
|
if (typeof formatted === 'string')
|
|
@@ -66,16 +75,20 @@ export function summarizeTaskChange(field, change, formatter) {
|
|
|
66
75
|
if (field === 'checklistItems') {
|
|
67
76
|
return summarizeChecklistChange(change);
|
|
68
77
|
}
|
|
78
|
+
const label = getActivityFieldLabel(field);
|
|
79
|
+
if (field === 'title') {
|
|
80
|
+
return `${label}: updated`;
|
|
81
|
+
}
|
|
69
82
|
if (field === 'description') {
|
|
70
83
|
const hadBefore = formatValue(change?.from, field, formatter) !== 'none';
|
|
71
84
|
const hasAfter = formatValue(change?.to, field, formatter) !== 'none';
|
|
72
85
|
if (!hadBefore && hasAfter)
|
|
73
|
-
return
|
|
86
|
+
return `${label}: added`;
|
|
74
87
|
if (hadBefore && !hasAfter)
|
|
75
|
-
return
|
|
76
|
-
return
|
|
88
|
+
return `${label}: cleared`;
|
|
89
|
+
return `${label}: updated`;
|
|
77
90
|
}
|
|
78
|
-
return `${
|
|
91
|
+
return `${label}: ${formatValue(change?.from, field, formatter)} -> ${formatValue(change?.to, field, formatter)}`;
|
|
79
92
|
}
|
|
80
93
|
export function summarizeTaskEvent(event, formatter) {
|
|
81
94
|
const changes = event.details?.changes || {};
|
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { summarizeTaskChange } from './taskActivity';
|
|
3
3
|
describe('summarizeTaskChange', () => {
|
|
4
|
+
it('summarizes title edits without dumping before and after text', () => {
|
|
5
|
+
expect(summarizeTaskChange('title', {
|
|
6
|
+
from: 'Old title',
|
|
7
|
+
to: 'New title',
|
|
8
|
+
})).toBe('TITLE: updated');
|
|
9
|
+
});
|
|
10
|
+
it('summarizes assignee changes with uppercase labels and formatted values', () => {
|
|
11
|
+
expect(summarizeTaskChange('assignee', { from: 'unassigned', to: 'member-1' }, (_field, value) => {
|
|
12
|
+
if (value === 'unassigned')
|
|
13
|
+
return 'Unassigned';
|
|
14
|
+
if (value === 'member-1')
|
|
15
|
+
return 'Emma Peel';
|
|
16
|
+
return undefined;
|
|
17
|
+
})).toBe('ASSIGNED TO: Unassigned -> Emma Peel');
|
|
18
|
+
});
|
|
19
|
+
it('summarizes description additions without rendering the body text', () => {
|
|
20
|
+
expect(summarizeTaskChange('description', {
|
|
21
|
+
from: '',
|
|
22
|
+
to: 'Line one\n\nLine two',
|
|
23
|
+
})).toBe('DESCRIPTION: added');
|
|
24
|
+
});
|
|
25
|
+
it('summarizes description clears without rendering the old body text', () => {
|
|
26
|
+
expect(summarizeTaskChange('description', {
|
|
27
|
+
from: 'Old body',
|
|
28
|
+
to: '',
|
|
29
|
+
})).toBe('DESCRIPTION: cleared');
|
|
30
|
+
});
|
|
4
31
|
it('summarizes checklist completion clearly', () => {
|
|
5
32
|
expect(summarizeTaskChange('checklistItems', {
|
|
6
33
|
from: [{ id: 'a', title: 'First item', isCompleted: false, order: 0 }],
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cloneDefaultPriorityDefinitions } from '../shared/taxonomyDefaults.js';
|
|
2
|
-
import { formatTaskReference } from './taskReferences.js';
|
|
2
|
+
import { formatLocalTaskReference, formatTaskReference } from './taskReferences.js';
|
|
3
3
|
export const DEFAULT_PRIORITY_DEFINITIONS = cloneDefaultPriorityDefinitions();
|
|
4
4
|
const PRIORITY_VALUE_BY_LABEL = {
|
|
5
5
|
low: 1,
|
|
@@ -75,12 +75,20 @@ export function normalizeTaskFromApi(task) {
|
|
|
75
75
|
: Number.isFinite(Number(task.referenceNumber))
|
|
76
76
|
? Number(task.referenceNumber)
|
|
77
77
|
: null;
|
|
78
|
+
const localReferenceNumber = typeof task.localReferenceNumber === 'number'
|
|
79
|
+
? task.localReferenceNumber
|
|
80
|
+
: Number.isFinite(Number(task.localReferenceNumber))
|
|
81
|
+
? Number(task.localReferenceNumber)
|
|
82
|
+
: null;
|
|
78
83
|
return {
|
|
79
84
|
...task,
|
|
80
85
|
referenceNumber: referenceNumber && referenceNumber > 0 ? Math.floor(referenceNumber) : null,
|
|
86
|
+
localReferenceNumber: localReferenceNumber && localReferenceNumber > 0 ? Math.floor(localReferenceNumber) : null,
|
|
81
87
|
referenceLabel: typeof task.referenceLabel === 'string' && task.referenceLabel.trim().length > 0
|
|
82
88
|
? task.referenceLabel.trim()
|
|
83
|
-
:
|
|
89
|
+
: (referenceNumber
|
|
90
|
+
? formatTaskReference(referenceNumber)
|
|
91
|
+
: formatLocalTaskReference(localReferenceNumber)),
|
|
84
92
|
assignee: (() => {
|
|
85
93
|
const raw = String(task.assignee || '').trim();
|
|
86
94
|
const normalized = raw.toLowerCase();
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import type { TaskItem } from '../types';
|
|
2
2
|
export declare const TASK_REFERENCE_PREFIX = "T-";
|
|
3
|
+
export declare const LOCAL_TASK_REFERENCE_PREFIX = "LT-";
|
|
4
|
+
export interface TaskReferenceDisplayState {
|
|
5
|
+
label: string;
|
|
6
|
+
isProvisional: boolean;
|
|
7
|
+
}
|
|
3
8
|
export declare function formatTaskReference(value: number | Pick<TaskItem, 'referenceNumber'> | null | undefined): string;
|
|
9
|
+
export declare function formatLocalTaskReference(value: number | Pick<TaskItem, 'localReferenceNumber'> | null | undefined): string;
|
|
4
10
|
export declare function parseTaskReference(value: string | null | undefined): number | null;
|
|
5
|
-
export declare function
|
|
11
|
+
export declare function parseLocalTaskReference(value: string | null | undefined): number | null;
|
|
12
|
+
export declare function getTaskReferenceLabel(task: Pick<TaskItem, 'referenceNumber' | 'referenceLabel' | 'localReferenceNumber'> | null | undefined): string;
|
|
13
|
+
export declare function isTaskReferenceProvisional(task: Pick<TaskItem, 'referenceNumber' | 'localReferenceNumber'> | null | undefined): boolean;
|
|
14
|
+
export declare function getTaskReferenceDisplayState(task: Pick<TaskItem, 'referenceNumber' | 'referenceLabel' | 'localReferenceNumber'> | null | undefined): TaskReferenceDisplayState;
|
|
15
|
+
export declare function shouldUseProvisionalTaskReferences(runtimeMode: string | null | undefined): boolean;
|
|
@@ -1,11 +1,43 @@
|
|
|
1
1
|
import { formatEntityReference, getEntityReferenceLabel, parseEntityReference, } from './entityReferences.js';
|
|
2
2
|
export const TASK_REFERENCE_PREFIX = 'T-';
|
|
3
|
+
export const LOCAL_TASK_REFERENCE_PREFIX = 'LT-';
|
|
3
4
|
export function formatTaskReference(value) {
|
|
4
5
|
return formatEntityReference(TASK_REFERENCE_PREFIX, value);
|
|
5
6
|
}
|
|
7
|
+
export function formatLocalTaskReference(value) {
|
|
8
|
+
if (typeof value === 'number' || value === null || value === undefined) {
|
|
9
|
+
return formatEntityReference(LOCAL_TASK_REFERENCE_PREFIX, value);
|
|
10
|
+
}
|
|
11
|
+
return formatEntityReference(LOCAL_TASK_REFERENCE_PREFIX, {
|
|
12
|
+
referenceNumber: value.localReferenceNumber ?? null
|
|
13
|
+
});
|
|
14
|
+
}
|
|
6
15
|
export function parseTaskReference(value) {
|
|
7
16
|
return parseEntityReference(TASK_REFERENCE_PREFIX, value);
|
|
8
17
|
}
|
|
18
|
+
export function parseLocalTaskReference(value) {
|
|
19
|
+
return parseEntityReference(LOCAL_TASK_REFERENCE_PREFIX, value);
|
|
20
|
+
}
|
|
9
21
|
export function getTaskReferenceLabel(task) {
|
|
10
|
-
|
|
22
|
+
if (!task)
|
|
23
|
+
return '';
|
|
24
|
+
if (typeof task.referenceLabel === 'string' && task.referenceLabel.trim().length > 0) {
|
|
25
|
+
return task.referenceLabel.trim();
|
|
26
|
+
}
|
|
27
|
+
const canonicalLabel = getEntityReferenceLabel(TASK_REFERENCE_PREFIX, task);
|
|
28
|
+
if (canonicalLabel)
|
|
29
|
+
return canonicalLabel;
|
|
30
|
+
return formatLocalTaskReference(task.localReferenceNumber);
|
|
31
|
+
}
|
|
32
|
+
export function isTaskReferenceProvisional(task) {
|
|
33
|
+
return !task?.referenceNumber && !!task?.localReferenceNumber;
|
|
34
|
+
}
|
|
35
|
+
export function getTaskReferenceDisplayState(task) {
|
|
36
|
+
return {
|
|
37
|
+
label: getTaskReferenceLabel(task),
|
|
38
|
+
isProvisional: isTaskReferenceProvisional(task)
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function shouldUseProvisionalTaskReferences(runtimeMode) {
|
|
42
|
+
return String(runtimeMode || '').trim().toLowerCase() === 'local';
|
|
11
43
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taskforcehq/taskforce",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.303",
|
|
4
4
|
"description": "Mission Command center for AI-powered task management (beta - under active development)",
|
|
5
5
|
"author": "Taskforce HQ <hello@taskforcehq.ai> (https://taskforcehq.ai)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,6 +37,8 @@
|
|
|
37
37
|
"perf": "sh scripts/run-dev-proxy.sh performance",
|
|
38
38
|
"stage": "sh scripts/run-dev-proxy.sh staging",
|
|
39
39
|
"prod": "TASKFORCE_RUNTIME_MODE=local TASKFORCE_CLOUD_ENVIRONMENT=production TASKFORCE_CLOUD_PROXY_BASE_URL=https://app.taskforcehq.ai VITE_TASKFORCE_BASE_URL=https://app.taskforcehq.ai npm run dev:runtime",
|
|
40
|
+
"deploy:staging": "bash scripts/deploy-staging.sh",
|
|
41
|
+
"deploy:prod": "bash scripts/deploy-prod.sh",
|
|
40
42
|
"dev:runtime": "concurrently \"npm run dev:app\" \"TASKFORCE_RUNTIME_MODE=local sh scripts/with-esbuild.sh node --import tsx src/cli.ts open 5174\"",
|
|
41
43
|
"stage:direct": "TASKFORCE_RUNTIME_MODE=local TASKFORCE_CLOUD_ENVIRONMENT=staging TASKFORCE_CLOUD_PROXY_BASE_URL=https://staging-app.taskforcehq.ai VITE_TASKFORCE_BASE_URL=https://staging-app.taskforcehq.ai npm run dev:runtime",
|
|
42
44
|
"perf:direct": "TASKFORCE_RUNTIME_MODE=local TASKFORCE_CLOUD_ENVIRONMENT=performance TASKFORCE_CLOUD_PROXY_BASE_URL=https://performance-app.taskforcehq.ai VITE_TASKFORCE_BASE_URL=https://performance-app.taskforcehq.ai npm run dev:runtime",
|
|
@@ -60,8 +62,6 @@
|
|
|
60
62
|
"check:localization-strings": "node scripts/check-localization-strings.mjs",
|
|
61
63
|
"version:next": "node scripts/release-version.mjs --dry-run",
|
|
62
64
|
"version:bump": "node scripts/release-version.mjs",
|
|
63
|
-
"deploy:staging": "bash scripts/deploy-staging.sh",
|
|
64
|
-
"deploy:prod": "bash scripts/deploy-prod.sh",
|
|
65
65
|
"test:localization": "vitest run src/__tests__/localizationCatalogs.test.ts",
|
|
66
66
|
"check:runtime-boundaries": "node scripts/check-runtime-boundaries.mjs",
|
|
67
67
|
"test:workflow-readiness": "npm run check:workflows && vitest src/__tests__/workflowSimulation.test.ts src/__tests__/workflowExportMatrix.test.ts src/__tests__/workflowExportSnapshots.test.ts src/__tests__/workflowDocumentFixtures.test.ts src/__tests__/structuredCommentValidation.test.ts src/__tests__/structuredDocValidation.test.ts src/__tests__/workflowV2Policy.test.ts src/__tests__/workflowGeneration.test.ts src/__tests__/templates.test.ts",
|
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
"test:e2e:realtime:reconnect": "npm run test:e2e:realtime -- tests/e2e/realtime-reconnect.spec.ts",
|
|
79
79
|
"playwright:auth:bootstrap": "node scripts/bootstrap-playwright-auth.mjs",
|
|
80
80
|
"playwright:auth": "sh scripts/playwright-auth.sh",
|
|
81
|
+
"qa:open": "node scripts/qa-open.mjs",
|
|
81
82
|
"check:env": "node --import tsx scripts/check-env.ts --target=server --strict=true",
|
|
82
83
|
"check:env:soak": "node --import tsx scripts/check-env.ts --target=soak --strict=true",
|
|
83
84
|
"check:compat": "node scripts/check-compat-boundaries.mjs",
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
export const QA_MODES = ['local', 'performance', 'staging'];
|
|
6
|
+
|
|
7
|
+
export const QA_CLOUD_BASE_URLS = {
|
|
8
|
+
performance: 'https://performance-app.taskforcehq.ai',
|
|
9
|
+
staging: 'https://staging-app.taskforcehq.ai'
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function normalizePort(raw) {
|
|
13
|
+
const value = String(raw || '').trim();
|
|
14
|
+
if (!value) return null;
|
|
15
|
+
const parsed = Number.parseInt(value, 10);
|
|
16
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return parsed;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function stripQuotes(raw) {
|
|
23
|
+
const value = String(raw || '').trim();
|
|
24
|
+
if (
|
|
25
|
+
(value.startsWith('"') && value.endsWith('"'))
|
|
26
|
+
|| (value.startsWith("'") && value.endsWith("'"))
|
|
27
|
+
) {
|
|
28
|
+
return value.slice(1, -1);
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function normalizeQaMode(raw) {
|
|
34
|
+
const normalized = String(raw || '').trim().toLowerCase();
|
|
35
|
+
return QA_MODES.includes(normalized) ? normalized : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function sanitizeQaAgentId(raw) {
|
|
39
|
+
const normalized = String(raw || '')
|
|
40
|
+
.trim()
|
|
41
|
+
.toLowerCase()
|
|
42
|
+
.replace(/[^a-z0-9_-]+/g, '-')
|
|
43
|
+
.replace(/^-+|-+$/g, '')
|
|
44
|
+
.replace(/-{2,}/g, '-');
|
|
45
|
+
return normalized || null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function parseQaArgs(argv, env = process.env) {
|
|
49
|
+
const positional = (argv || []).filter((arg) => !String(arg || '').startsWith('--'));
|
|
50
|
+
const mode = normalizeQaMode(positional[0] || env.TASKFORCE_QA_MODE || 'local');
|
|
51
|
+
const preferredPort = normalizePort(positional[1] || env.TASKFORCE_QA_PORT);
|
|
52
|
+
const agentId = sanitizeQaAgentId(env.TASKFORCE_QA_AGENT);
|
|
53
|
+
return { mode, agentId, preferredPort };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function resolveQaPaths({ agentId, homeDir = os.homedir() }) {
|
|
57
|
+
const safeAgentId = sanitizeQaAgentId(agentId);
|
|
58
|
+
if (!safeAgentId) {
|
|
59
|
+
throw new Error('TASKFORCE_QA_AGENT is required and must contain at least one safe path character.');
|
|
60
|
+
}
|
|
61
|
+
const root = path.resolve(homeDir, '.taskforce', 'qa', safeAgentId);
|
|
62
|
+
return {
|
|
63
|
+
agentId: safeAgentId,
|
|
64
|
+
root,
|
|
65
|
+
projectRoot: path.join(root, 'project'),
|
|
66
|
+
envFile: path.join(root, 'env'),
|
|
67
|
+
logsDir: path.join(root, 'logs'),
|
|
68
|
+
artifactsDir: path.join(root, 'artifacts'),
|
|
69
|
+
homeDir: path.join(root, 'home'),
|
|
70
|
+
playwrightStorageState: path.join(root, 'playwright-auth.json')
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function isPathInside(parentPath, childPath) {
|
|
75
|
+
const parent = path.resolve(parentPath);
|
|
76
|
+
const child = path.resolve(childPath);
|
|
77
|
+
const relative = path.relative(parent, child);
|
|
78
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function assertQaProjectRootSafe(projectRoot, repoRoot) {
|
|
82
|
+
if (isPathInside(repoRoot, projectRoot)) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Refusing to use QA project root inside the repository: ${projectRoot}\n`
|
|
85
|
+
+ `Move the QA root outside ${repoRoot}.`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function parseEnvFileContents(raw) {
|
|
91
|
+
const env = {};
|
|
92
|
+
const lines = String(raw || '').split(/\r?\n/);
|
|
93
|
+
for (const line of lines) {
|
|
94
|
+
const trimmed = line.trim();
|
|
95
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
96
|
+
const normalized = trimmed.startsWith('export ') ? trimmed.slice(7).trim() : trimmed;
|
|
97
|
+
const separatorIndex = normalized.indexOf('=');
|
|
98
|
+
if (separatorIndex <= 0) continue;
|
|
99
|
+
const key = normalized.slice(0, separatorIndex).trim();
|
|
100
|
+
if (!key) continue;
|
|
101
|
+
const value = normalized.slice(separatorIndex + 1);
|
|
102
|
+
env[key] = stripQuotes(value);
|
|
103
|
+
}
|
|
104
|
+
return env;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function loadEnvFile(filePath) {
|
|
108
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
109
|
+
return parseEnvFileContents(raw);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function ensureQaRootStructure(paths, options = {}) {
|
|
113
|
+
const projectName = String(options.projectName || `taskforce-qa-${paths.agentId}`).trim();
|
|
114
|
+
fs.mkdirSync(paths.root, { recursive: true });
|
|
115
|
+
fs.mkdirSync(paths.projectRoot, { recursive: true });
|
|
116
|
+
fs.mkdirSync(paths.logsDir, { recursive: true });
|
|
117
|
+
fs.mkdirSync(paths.artifactsDir, { recursive: true });
|
|
118
|
+
fs.mkdirSync(paths.homeDir, { recursive: true });
|
|
119
|
+
|
|
120
|
+
const packageJsonPath = path.join(paths.projectRoot, 'package.json');
|
|
121
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
122
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify({ name: projectName }, null, 2) + '\n', 'utf8');
|
|
123
|
+
}
|
|
124
|
+
return packageJsonPath;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function resolveExpectedWorkspace(env) {
|
|
128
|
+
const workspaceId = String(env.TASKFORCE_QA_EXPECTED_WORKSPACE_ID || '').trim();
|
|
129
|
+
const workspaceSlug = String(env.TASKFORCE_QA_EXPECTED_WORKSPACE_SLUG || '').trim();
|
|
130
|
+
if (!workspaceId && !workspaceSlug) return null;
|
|
131
|
+
return {
|
|
132
|
+
workspaceId: workspaceId || null,
|
|
133
|
+
workspaceSlug: workspaceSlug || null
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function matchExpectedWorkspace(result, expected) {
|
|
138
|
+
const nestedWorkspaces = Array.isArray(result?.workspaces?.workspaces) ? result.workspaces.workspaces : [];
|
|
139
|
+
const workspaces = Array.isArray(result?.workspaces) ? result.workspaces : nestedWorkspaces;
|
|
140
|
+
const currentWorkspaceId = String(result?.currentWorkspaceId || '').trim();
|
|
141
|
+
if (!expected) {
|
|
142
|
+
return {
|
|
143
|
+
ok: true,
|
|
144
|
+
matchedWorkspaceId: currentWorkspaceId || null,
|
|
145
|
+
reason: 'No expected workspace configured.'
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (expected.workspaceId) {
|
|
150
|
+
return {
|
|
151
|
+
ok: currentWorkspaceId === expected.workspaceId,
|
|
152
|
+
matchedWorkspaceId: currentWorkspaceId || null,
|
|
153
|
+
reason: currentWorkspaceId === expected.workspaceId
|
|
154
|
+
? 'Matched expected workspace id.'
|
|
155
|
+
: `Expected workspace id ${expected.workspaceId} but current workspace id was ${currentWorkspaceId || '(empty)'}.`
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const matchedBySlug = workspaces.find((workspace) => String(workspace?.slug || '').trim() === expected.workspaceSlug);
|
|
160
|
+
if (!matchedBySlug) {
|
|
161
|
+
return {
|
|
162
|
+
ok: false,
|
|
163
|
+
matchedWorkspaceId: currentWorkspaceId || null,
|
|
164
|
+
reason: `Expected workspace slug ${expected.workspaceSlug} was not present in the available workspaces list.`
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
ok: currentWorkspaceId === String(matchedBySlug.id || '').trim(),
|
|
170
|
+
matchedWorkspaceId: currentWorkspaceId || null,
|
|
171
|
+
reason: currentWorkspaceId === String(matchedBySlug.id || '').trim()
|
|
172
|
+
? 'Matched expected workspace slug.'
|
|
173
|
+
: `Expected workspace slug ${expected.workspaceSlug} to resolve to workspace id ${String(matchedBySlug.id || '').trim()}, but current workspace id was ${currentWorkspaceId || '(empty)'}.`
|
|
174
|
+
};
|
|
175
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export const QA_MODES: string[];
|
|
2
|
+
export const QA_CLOUD_BASE_URLS: Record<string, string>;
|
|
3
|
+
|
|
4
|
+
export function normalizeQaMode(raw: unknown): string | null;
|
|
5
|
+
export function sanitizeQaAgentId(raw: unknown): string | null;
|
|
6
|
+
export function parseQaArgs(
|
|
7
|
+
argv?: string[],
|
|
8
|
+
env?: Record<string, string | undefined>
|
|
9
|
+
): { mode: string | null; agentId: string | null; preferredPort: number | null };
|
|
10
|
+
export function resolveQaPaths(input: {
|
|
11
|
+
agentId: string;
|
|
12
|
+
homeDir?: string;
|
|
13
|
+
}): {
|
|
14
|
+
agentId: string;
|
|
15
|
+
root: string;
|
|
16
|
+
projectRoot: string;
|
|
17
|
+
envFile: string;
|
|
18
|
+
logsDir: string;
|
|
19
|
+
artifactsDir: string;
|
|
20
|
+
homeDir: string;
|
|
21
|
+
playwrightStorageState: string;
|
|
22
|
+
};
|
|
23
|
+
export function isPathInside(parentPath: string, childPath: string): boolean;
|
|
24
|
+
export function assertQaProjectRootSafe(projectRoot: string, repoRoot: string): void;
|
|
25
|
+
export function parseEnvFileContents(raw: unknown): Record<string, string>;
|
|
26
|
+
export function loadEnvFile(filePath: string): Record<string, string>;
|
|
27
|
+
export function ensureQaRootStructure(
|
|
28
|
+
paths: {
|
|
29
|
+
agentId: string;
|
|
30
|
+
root: string;
|
|
31
|
+
projectRoot: string;
|
|
32
|
+
logsDir: string;
|
|
33
|
+
artifactsDir: string;
|
|
34
|
+
homeDir: string;
|
|
35
|
+
},
|
|
36
|
+
options?: { projectName?: string }
|
|
37
|
+
): string;
|
|
38
|
+
export function resolveExpectedWorkspace(env: Record<string, string | undefined>): {
|
|
39
|
+
workspaceId: string | null;
|
|
40
|
+
workspaceSlug: string | null;
|
|
41
|
+
} | null;
|
|
42
|
+
export function matchExpectedWorkspace(
|
|
43
|
+
result: unknown,
|
|
44
|
+
expected: { workspaceId: string | null; workspaceSlug: string | null } | null
|
|
45
|
+
): {
|
|
46
|
+
ok: boolean;
|
|
47
|
+
matchedWorkspaceId: string | null;
|
|
48
|
+
reason: string;
|
|
49
|
+
};
|