@markjaquith/agency 3.2.2 → 3.2.4
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/README.md +30 -62
- package/cli-main.ts +20 -83
- package/fixtures/protocol/orchestration-recipes.json +9 -34
- package/package.json +1 -4
- package/schemas/agency-graph-v1.schema.json +2 -31
- package/src/cli-parser.test.ts +34 -132
- package/src/cli-parser.ts +45 -102
- package/src/cli.test.ts +60 -117
- package/src/commands/act.ts +2 -6
- package/src/commands/sync.ts +3 -3
- package/src/commands/validate.ts +3 -1
- package/src/commands/work.test.ts +70 -8
- package/src/commands/work.ts +15 -8
- package/src/graph-schema.ts +0 -2
- package/src/protocol.test.ts +0 -25
- package/src/protocol.ts +0 -11
- package/src/readiness.test.ts +2 -2
- package/src/services/ArchiveBulkService.test.ts +0 -88
- package/src/services/ArchiveService.ts +0 -32
- package/src/services/GraphMutationService.ts +0 -26
- package/src/services/GraphService.test.ts +1 -1
- package/src/services/IntegrationService.test.ts +7 -4
- package/src/services/LifecycleTransaction.ts +5 -1
- package/src/services/PhaseService.ts +1 -8
- package/src/services/ReadinessService.test.ts +0 -34
- package/src/services/ReadinessService.ts +1 -20
- package/src/services/ReviewService.test.ts +0 -64
- package/src/services/ReviewService.ts +0 -5
- package/src/services/SyncService.test.ts +75 -88
- package/src/services/SyncService.ts +52 -123
- package/src/services/TaskPhaseService.test.ts +13 -1
- package/src/services/TaskService.ts +1 -7
- package/src/services/WorkbaseService.ts +0 -3
- package/src/services/WorktreeService.test.ts +192 -1
- package/src/services/WorktreeService.ts +169 -34
- package/src/test-utils.ts +0 -2
- package/src/usage-log.test.ts +89 -5
- package/src/usage-log.ts +139 -31
- package/src/workbase/AGENTS.md +9 -15
- package/src/workbase/agent-command.test.ts +1 -3
- package/src/workbase/agent-command.ts +1 -6
- package/src/workbase/document-revision.ts +0 -4
- package/src/workbase/opencode-plugin-file.ts +1 -0
- package/src/workbase/schemas.test.ts +12 -25
- package/src/workbase/schemas.ts +0 -16
- package/src/commands/claim.ts +0 -122
- package/src/services/ClaimService.test.ts +0 -415
- package/src/services/ClaimService.ts +0 -608
|
@@ -182,6 +182,14 @@ const formatCommand = (args: readonly string[]) =>
|
|
|
182
182
|
)
|
|
183
183
|
.join(" ")
|
|
184
184
|
|
|
185
|
+
const describeError = (error: unknown) =>
|
|
186
|
+
typeof error === "object" &&
|
|
187
|
+
error !== null &&
|
|
188
|
+
"message" in error &&
|
|
189
|
+
typeof error.message === "string"
|
|
190
|
+
? error.message
|
|
191
|
+
: String(error)
|
|
192
|
+
|
|
185
193
|
const runPostCheckoutHook = (options: {
|
|
186
194
|
readonly command: readonly string[] | undefined
|
|
187
195
|
readonly variables: CheckoutCommandVariables
|
|
@@ -1871,6 +1879,10 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
1871
1879
|
}
|
|
1872
1880
|
| { review: { repo: string; commit: string } }
|
|
1873
1881
|
let codePath: string
|
|
1882
|
+
let executionState: {
|
|
1883
|
+
readonly status: string
|
|
1884
|
+
readonly claim?: { readonly state: string }
|
|
1885
|
+
}
|
|
1874
1886
|
if ("phases" in task.data) {
|
|
1875
1887
|
if (!phaseId) {
|
|
1876
1888
|
return yield* new WorktreeError({
|
|
@@ -1880,6 +1892,7 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
1880
1892
|
const phase =
|
|
1881
1893
|
options.phase ?? (yield* phases.show(taskId, phaseId, root))
|
|
1882
1894
|
execution = phase.data
|
|
1895
|
+
executionState = phase.data
|
|
1883
1896
|
codePath = join(dirname(phase.path), "code")
|
|
1884
1897
|
} else {
|
|
1885
1898
|
if (phaseId) {
|
|
@@ -1888,8 +1901,25 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
1888
1901
|
})
|
|
1889
1902
|
}
|
|
1890
1903
|
execution = task.data
|
|
1904
|
+
executionState = task.data
|
|
1891
1905
|
codePath = join(dirname(task.path), "code")
|
|
1892
1906
|
}
|
|
1907
|
+
const ownerLabel = phaseId
|
|
1908
|
+
? `Phase '${taskId}/${phaseId}'`
|
|
1909
|
+
: `Task '${taskId}'`
|
|
1910
|
+
if (executionState.claim?.state === "active") {
|
|
1911
|
+
return yield* new WorktreeError({
|
|
1912
|
+
message: `${ownerLabel} has an active claim; release or finish it before removing its worktrees`,
|
|
1913
|
+
})
|
|
1914
|
+
}
|
|
1915
|
+
if (
|
|
1916
|
+
executionState.status === "working" ||
|
|
1917
|
+
executionState.status === "delegated"
|
|
1918
|
+
) {
|
|
1919
|
+
return yield* new WorktreeError({
|
|
1920
|
+
message: `${ownerLabel} has active '${executionState.status}' ownership; reopen it before removing its worktrees`,
|
|
1921
|
+
})
|
|
1922
|
+
}
|
|
1893
1923
|
|
|
1894
1924
|
const codeDirectoryExists = yield* fs.isDirectory(codePath)
|
|
1895
1925
|
const removalPlans: {
|
|
@@ -1931,6 +1961,11 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
1931
1961
|
const alias = checkout.repo
|
|
1932
1962
|
const repositoryPath = join(root, "repos", alias)
|
|
1933
1963
|
const checkoutPath = join(codePath, alias)
|
|
1964
|
+
if ((yield* fs.readSymlinkTarget(checkoutPath)) !== null) {
|
|
1965
|
+
return yield* new WorktreeError({
|
|
1966
|
+
message: `Cannot remove ${codePath}; expected checkout ${checkoutPath} is a symbolic link`,
|
|
1967
|
+
})
|
|
1968
|
+
}
|
|
1934
1969
|
if (
|
|
1935
1970
|
(yield* fs.exists(checkoutPath)) &&
|
|
1936
1971
|
!(yield* fs.isDirectory(checkoutPath))
|
|
@@ -2024,7 +2059,12 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
2024
2059
|
["git", "-C", checkoutPath, "status", "--porcelain"],
|
|
2025
2060
|
{ captureOutput: true },
|
|
2026
2061
|
)
|
|
2027
|
-
if (status.exitCode !== 0
|
|
2062
|
+
if (status.exitCode !== 0) {
|
|
2063
|
+
return yield* new WorktreeError({
|
|
2064
|
+
message: `Failed to remove worktree for '${alias}': checkout cleanliness could not be verified: ${status.stderr.trim() || `git status exited with code ${status.exitCode}`}`,
|
|
2065
|
+
})
|
|
2066
|
+
}
|
|
2067
|
+
if (status.stdout.trim()) {
|
|
2028
2068
|
return yield* new WorktreeError({
|
|
2029
2069
|
message: `Failed to remove worktree for '${alias}': checkout has uncommitted changes`,
|
|
2030
2070
|
})
|
|
@@ -2039,6 +2079,25 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
2039
2079
|
head: registered.head,
|
|
2040
2080
|
branch: registered.branch?.replace(/^refs\/heads\//, ""),
|
|
2041
2081
|
})
|
|
2082
|
+
if (!checkoutExists) {
|
|
2083
|
+
const unrelatedStale: string[] = []
|
|
2084
|
+
for (const worktree of parseWorktreeList(listed.stdout)) {
|
|
2085
|
+
const worktreePath = (yield* fs.exists(worktree.path))
|
|
2086
|
+
? yield* fs.realPath(worktree.path)
|
|
2087
|
+
: resolve(worktree.path)
|
|
2088
|
+
if (
|
|
2089
|
+
worktreePath !== registered.path &&
|
|
2090
|
+
!(yield* fs.exists(worktree.path))
|
|
2091
|
+
) {
|
|
2092
|
+
unrelatedStale.push(worktree.path)
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
if (unrelatedStale.length > 0) {
|
|
2096
|
+
return yield* new WorktreeError({
|
|
2097
|
+
message: `Cannot remove stale registration ${registered.path} because pruning would also remove unrelated stale registrations: ${unrelatedStale.join(", ")}`,
|
|
2098
|
+
})
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2042
2101
|
}
|
|
2043
2102
|
for (const plan of removalPlans) {
|
|
2044
2103
|
if (!plan.checkoutExists || !plan.head) continue
|
|
@@ -2056,6 +2115,38 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
2056
2115
|
}
|
|
2057
2116
|
|
|
2058
2117
|
const completed: typeof removalPlans = []
|
|
2118
|
+
const planState = (plan: (typeof removalPlans)[number]) =>
|
|
2119
|
+
Effect.gen(function* () {
|
|
2120
|
+
const checkoutExists = yield* fs.isDirectory(plan.checkoutPath)
|
|
2121
|
+
const listed = yield* fs.runCommand(
|
|
2122
|
+
[
|
|
2123
|
+
"git",
|
|
2124
|
+
"-C",
|
|
2125
|
+
plan.repositoryPath,
|
|
2126
|
+
"worktree",
|
|
2127
|
+
"list",
|
|
2128
|
+
"--porcelain",
|
|
2129
|
+
"-z",
|
|
2130
|
+
],
|
|
2131
|
+
{ captureOutput: true },
|
|
2132
|
+
)
|
|
2133
|
+
if (listed.exitCode !== 0) {
|
|
2134
|
+
return yield* new WorktreeError({
|
|
2135
|
+
message: `Failed to inspect worktrees for '${plan.alias}' during recovery: ${listed.stderr.trim() || `git worktree list exited with code ${listed.exitCode}`}`,
|
|
2136
|
+
})
|
|
2137
|
+
}
|
|
2138
|
+
let registered = false
|
|
2139
|
+
for (const worktree of parseWorktreeList(listed.stdout)) {
|
|
2140
|
+
const worktreePath = (yield* fs.exists(worktree.path))
|
|
2141
|
+
? yield* fs.realPath(worktree.path)
|
|
2142
|
+
: resolve(worktree.path)
|
|
2143
|
+
if (worktreePath === plan.registeredPath) {
|
|
2144
|
+
registered = true
|
|
2145
|
+
break
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
return { checkoutExists, registered }
|
|
2149
|
+
})
|
|
2059
2150
|
const removed = yield* Effect.gen(function* () {
|
|
2060
2151
|
for (const plan of removalPlans) {
|
|
2061
2152
|
const command = plan.checkoutExists
|
|
@@ -2080,8 +2171,18 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
2080
2171
|
captureOutput: true,
|
|
2081
2172
|
})
|
|
2082
2173
|
if (result.exitCode !== 0) {
|
|
2174
|
+
const state = yield* planState(plan).pipe(
|
|
2175
|
+
Effect.catchAll(() => Effect.succeed(undefined)),
|
|
2176
|
+
)
|
|
2177
|
+
if (
|
|
2178
|
+
!state ||
|
|
2179
|
+
state.checkoutExists !== plan.checkoutExists ||
|
|
2180
|
+
!state.registered
|
|
2181
|
+
) {
|
|
2182
|
+
completed.push(plan)
|
|
2183
|
+
}
|
|
2083
2184
|
return yield* new WorktreeError({
|
|
2084
|
-
message: `Failed to remove worktree for '${plan.alias}': ${result.stderr}`,
|
|
2185
|
+
message: `Failed to remove worktree for '${plan.alias}': ${result.stderr.trim() || `git exited with code ${result.exitCode}`}`,
|
|
2085
2186
|
})
|
|
2086
2187
|
}
|
|
2087
2188
|
completed.push(plan)
|
|
@@ -2099,43 +2200,77 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
2099
2200
|
for (const plan of [...completed].reverse()) {
|
|
2100
2201
|
if (!plan.checkoutExists) {
|
|
2101
2202
|
manualRecovery.push(
|
|
2102
|
-
`
|
|
2203
|
+
`Restore stale registration ${plan.registeredPath} if it is still needed`,
|
|
2103
2204
|
)
|
|
2104
2205
|
continue
|
|
2105
2206
|
}
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
plan.checkoutPath
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2207
|
+
const recovery = Effect.gen(function* () {
|
|
2208
|
+
const state = yield* planState(plan)
|
|
2209
|
+
if (state.checkoutExists && state.registered) {
|
|
2210
|
+
rolledBack.push(plan.checkoutPath)
|
|
2211
|
+
return
|
|
2212
|
+
}
|
|
2213
|
+
if (state.checkoutExists || state.registered) {
|
|
2214
|
+
manualRecovery.push(
|
|
2215
|
+
`Restore ${plan.checkoutPath}; checkout path ${state.checkoutExists ? "exists" : "is missing"} and registration ${state.registered ? "exists" : "is missing"}`,
|
|
2216
|
+
)
|
|
2217
|
+
return
|
|
2218
|
+
}
|
|
2219
|
+
yield* fs.createDirectory(dirname(plan.checkoutPath))
|
|
2220
|
+
const command = plan.branch
|
|
2221
|
+
? [
|
|
2222
|
+
"git",
|
|
2223
|
+
"-C",
|
|
2224
|
+
plan.repositoryPath,
|
|
2225
|
+
"worktree",
|
|
2226
|
+
"add",
|
|
2227
|
+
plan.checkoutPath,
|
|
2228
|
+
plan.branch,
|
|
2229
|
+
]
|
|
2230
|
+
: [
|
|
2231
|
+
"git",
|
|
2232
|
+
"-C",
|
|
2233
|
+
plan.repositoryPath,
|
|
2234
|
+
"worktree",
|
|
2235
|
+
"add",
|
|
2236
|
+
"--detach",
|
|
2237
|
+
plan.checkoutPath,
|
|
2238
|
+
plan.head!,
|
|
2239
|
+
]
|
|
2240
|
+
const restored = yield* fs.runCommand(command, {
|
|
2241
|
+
captureOutput: true,
|
|
2242
|
+
})
|
|
2243
|
+
if (restored.exitCode === 0) {
|
|
2244
|
+
rolledBack.push(plan.checkoutPath)
|
|
2245
|
+
} else {
|
|
2246
|
+
manualRecovery.push(
|
|
2247
|
+
`Restore ${plan.checkoutPath}: ${restored.stderr.trim() || `git exited with code ${restored.exitCode}`}`,
|
|
2248
|
+
)
|
|
2249
|
+
}
|
|
2250
|
+
}).pipe(
|
|
2251
|
+
Effect.catchAll((recoveryCause) =>
|
|
2252
|
+
Effect.sync(() => {
|
|
2253
|
+
manualRecovery.push(
|
|
2254
|
+
`Restore ${plan.checkoutPath}: ${describeError(recoveryCause)}`,
|
|
2255
|
+
)
|
|
2256
|
+
}),
|
|
2257
|
+
),
|
|
2258
|
+
)
|
|
2259
|
+
yield* recovery
|
|
2133
2260
|
}
|
|
2261
|
+
const causeMessage = describeError(cause)
|
|
2134
2262
|
return yield* new WorktreeError({
|
|
2135
|
-
message:
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2263
|
+
message:
|
|
2264
|
+
completed.length === 0
|
|
2265
|
+
? `${causeMessage}; no worktrees were removed`
|
|
2266
|
+
: manualRecovery.length
|
|
2267
|
+
? `${causeMessage}. Worktree removal requires manual recovery`
|
|
2268
|
+
: `${causeMessage}. Removed worktrees were restored`,
|
|
2269
|
+
completed: completed.map((plan) =>
|
|
2270
|
+
plan.checkoutExists
|
|
2271
|
+
? plan.checkoutPath
|
|
2272
|
+
: plan.registeredPath,
|
|
2273
|
+
),
|
|
2139
2274
|
rolledBack,
|
|
2140
2275
|
manualRecovery,
|
|
2141
2276
|
cause,
|
package/src/test-utils.ts
CHANGED
|
@@ -16,7 +16,6 @@ import { ArchiveService } from "./services/ArchiveService"
|
|
|
16
16
|
import { IntegrationService } from "./services/IntegrationService"
|
|
17
17
|
import { ContextService } from "./services/ContextService"
|
|
18
18
|
import { GraphService } from "./services/GraphService"
|
|
19
|
-
import { ClaimService } from "./services/ClaimService"
|
|
20
19
|
import { SyncService } from "./services/SyncService"
|
|
21
20
|
import { ReadinessService } from "./services/ReadinessService"
|
|
22
21
|
import { GraphMutationService } from "./services/GraphMutationService"
|
|
@@ -48,7 +47,6 @@ const TestLayer = Layer.mergeAll(
|
|
|
48
47
|
IntegrationService.Default,
|
|
49
48
|
ContextService.Default,
|
|
50
49
|
GraphService.Default,
|
|
51
|
-
ClaimService.Default,
|
|
52
50
|
SyncService.Default,
|
|
53
51
|
ReadinessService.Default,
|
|
54
52
|
GraphMutationService.Default,
|
package/src/usage-log.test.ts
CHANGED
|
@@ -1,20 +1,36 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test"
|
|
2
|
+
import { Database } from "bun:sqlite"
|
|
2
3
|
import { rm } from "node:fs/promises"
|
|
3
4
|
import { join } from "node:path"
|
|
4
5
|
import { cleanupTempDir, createTempDir } from "./test-utils"
|
|
5
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
exportUsageEvents,
|
|
8
|
+
recordUsageEvent,
|
|
9
|
+
usageOutcomeCode,
|
|
10
|
+
} from "./usage-log"
|
|
6
11
|
|
|
7
12
|
describe("usage logging", () => {
|
|
8
13
|
const tempDirs: string[] = []
|
|
9
14
|
|
|
10
15
|
afterEach(() => Promise.all(tempDirs.splice(0).map(cleanupTempDir)))
|
|
11
16
|
|
|
17
|
+
test("maps only reviewed failure categories", () => {
|
|
18
|
+
expect(usageOutcomeCode({ _tag: "WorkbaseNotFoundError" })).toBe(
|
|
19
|
+
"WORKBASE_NOT_FOUND",
|
|
20
|
+
)
|
|
21
|
+
expect(usageOutcomeCode({ _tag: "private-customer-id" })).toBe(
|
|
22
|
+
"COMMAND_FAILED",
|
|
23
|
+
)
|
|
24
|
+
})
|
|
25
|
+
|
|
12
26
|
test("stores versioned privacy-safe events in session order", async () => {
|
|
13
27
|
const state = await createTempDir()
|
|
14
28
|
tempDirs.push(state)
|
|
15
29
|
const env = {
|
|
16
30
|
XDG_STATE_HOME: state,
|
|
17
31
|
AGENCY_SESSION_ID: "session-1",
|
|
32
|
+
AGENCY_INVOCATION_SOURCE: "automation",
|
|
33
|
+
AGENCY_USAGE_TEST: "1",
|
|
18
34
|
} as NodeJS.ProcessEnv
|
|
19
35
|
for (const commandPath of ["worktree/prepare", "context"]) {
|
|
20
36
|
await recordUsageEvent(
|
|
@@ -23,6 +39,7 @@ describe("usage logging", () => {
|
|
|
23
39
|
flagNames: ["json", "task", "json"],
|
|
24
40
|
durationMs: 12.4,
|
|
25
41
|
outcome: "success",
|
|
42
|
+
outcomeCode: "SUCCESS",
|
|
26
43
|
exitStatus: 0,
|
|
27
44
|
...(commandPath === "context"
|
|
28
45
|
? {
|
|
@@ -40,26 +57,31 @@ describe("usage logging", () => {
|
|
|
40
57
|
expect(await Bun.file(join(state, "agency/usage.sqlite3")).exists()).toBe(
|
|
41
58
|
true,
|
|
42
59
|
)
|
|
43
|
-
|
|
60
|
+
const events = await exportUsageEvents(env)
|
|
61
|
+
expect(events).toEqual([
|
|
44
62
|
expect.objectContaining({
|
|
45
63
|
version: 2,
|
|
46
|
-
|
|
47
|
-
|
|
64
|
+
journeyId: expect.stringMatching(/^sha256:[a-f0-9]{64}$/),
|
|
65
|
+
journeySequence: 1,
|
|
66
|
+
invocationSource: "automation",
|
|
67
|
+
isTest: true,
|
|
48
68
|
agencyVersion: "1.2.3",
|
|
49
69
|
commandPath: "worktree/prepare",
|
|
50
70
|
flagNames: ["json", "task"],
|
|
51
71
|
durationMs: 12,
|
|
52
72
|
outcome: "success",
|
|
73
|
+
outcomeCode: "SUCCESS",
|
|
53
74
|
exitStatus: 0,
|
|
54
75
|
}),
|
|
55
76
|
expect.objectContaining({
|
|
56
|
-
|
|
77
|
+
journeySequence: 2,
|
|
57
78
|
commandPath: "context",
|
|
58
79
|
vcs: "git",
|
|
59
80
|
terminalStage: "publish",
|
|
60
81
|
category: "success",
|
|
61
82
|
}),
|
|
62
83
|
])
|
|
84
|
+
expect(JSON.stringify(events)).not.toContain("session-1")
|
|
63
85
|
})
|
|
64
86
|
|
|
65
87
|
test("supports opt-out and ignores unavailable storage", async () => {
|
|
@@ -75,6 +97,7 @@ describe("usage logging", () => {
|
|
|
75
97
|
flagNames: [],
|
|
76
98
|
durationMs: 1,
|
|
77
99
|
outcome: "failure",
|
|
100
|
+
outcomeCode: "COMMAND_FAILED",
|
|
78
101
|
exitStatus: 1,
|
|
79
102
|
},
|
|
80
103
|
"1.2.3",
|
|
@@ -93,6 +116,7 @@ describe("usage logging", () => {
|
|
|
93
116
|
flagNames: [],
|
|
94
117
|
durationMs: 1,
|
|
95
118
|
outcome: "failure",
|
|
119
|
+
outcomeCode: "COMMAND_FAILED",
|
|
96
120
|
exitStatus: 1,
|
|
97
121
|
},
|
|
98
122
|
"1.2.3",
|
|
@@ -101,4 +125,64 @@ describe("usage logging", () => {
|
|
|
101
125
|
).resolves.toBeUndefined()
|
|
102
126
|
await rm(blocked)
|
|
103
127
|
})
|
|
128
|
+
|
|
129
|
+
test("prunes expired events on every database access", async () => {
|
|
130
|
+
const state = await createTempDir()
|
|
131
|
+
tempDirs.push(state)
|
|
132
|
+
const env = {
|
|
133
|
+
XDG_STATE_HOME: state,
|
|
134
|
+
AGENCY_USAGE_RETENTION_DAYS: "30",
|
|
135
|
+
} as NodeJS.ProcessEnv
|
|
136
|
+
await recordUsageEvent(
|
|
137
|
+
{
|
|
138
|
+
commandPath: "status",
|
|
139
|
+
flagNames: [],
|
|
140
|
+
durationMs: 1,
|
|
141
|
+
outcome: "success",
|
|
142
|
+
outcomeCode: "SUCCESS",
|
|
143
|
+
exitStatus: 0,
|
|
144
|
+
},
|
|
145
|
+
"1.2.3",
|
|
146
|
+
env,
|
|
147
|
+
)
|
|
148
|
+
const database = new Database(join(state, "agency/usage.sqlite3"))
|
|
149
|
+
database.run(
|
|
150
|
+
"UPDATE usage_events SET occurred_at = datetime('now', '-31 days')",
|
|
151
|
+
)
|
|
152
|
+
database.close()
|
|
153
|
+
|
|
154
|
+
expect(await exportUsageEvents(env)).toEqual([])
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test("removes legacy events that may contain positional values", async () => {
|
|
158
|
+
const state = await createTempDir()
|
|
159
|
+
tempDirs.push(state)
|
|
160
|
+
const path = join(state, "usage.sqlite3")
|
|
161
|
+
const database = new Database(path, { create: true })
|
|
162
|
+
database.run(
|
|
163
|
+
"CREATE TABLE usage_events (id INTEGER PRIMARY KEY, command_path TEXT NOT NULL)",
|
|
164
|
+
)
|
|
165
|
+
database.run("INSERT INTO usage_events (command_path) VALUES (?)", [
|
|
166
|
+
"task/private-customer-id",
|
|
167
|
+
])
|
|
168
|
+
database.close()
|
|
169
|
+
const env = { AGENCY_USAGE_DB: path } as NodeJS.ProcessEnv
|
|
170
|
+
|
|
171
|
+
await recordUsageEvent(
|
|
172
|
+
{
|
|
173
|
+
commandPath: "task/show",
|
|
174
|
+
flagNames: [],
|
|
175
|
+
durationMs: 1,
|
|
176
|
+
outcome: "success",
|
|
177
|
+
outcomeCode: "SUCCESS",
|
|
178
|
+
exitStatus: 0,
|
|
179
|
+
},
|
|
180
|
+
"1.2.3",
|
|
181
|
+
env,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
expect(await exportUsageEvents(env)).toEqual([
|
|
185
|
+
expect.objectContaining({ commandPath: "task/show", version: 2 }),
|
|
186
|
+
])
|
|
187
|
+
})
|
|
104
188
|
})
|