@defrex/autobuild 0.8.0 → 0.10.0
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 +1 -1
- package/SPEC.md +323 -26
- package/bin/ab-build-runner.ts +8 -2
- package/docs/architecture.md +166 -14
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +175 -28
- package/docs/hosted-dispatcher.md +69 -13
- package/docs/operator-api.md +2 -2
- package/docs/remote-store-protocol.md +204 -16
- package/docs/remote-ticket-protocol.md +1 -1
- package/docs/setup.md +12 -9
- package/docs/website.md +14 -0
- package/package.json +45 -23
- package/packages/core/src/cli/artifact.ts +130 -6
- package/packages/core/src/cli/binary.ts +9 -1
- package/packages/core/src/cli/build-control.ts +110 -13
- package/packages/core/src/cli/context.ts +46 -0
- package/packages/core/src/cli/dashboard/facts.ts +215 -0
- package/packages/core/src/cli/dashboard/frame-parse.ts +223 -0
- package/packages/core/src/cli/dashboard/model.ts +141 -153
- package/packages/core/src/cli/dashboard/poll.ts +76 -12
- package/packages/core/src/cli/dashboard/render.ts +34 -5
- package/packages/core/src/cli/dispatch-frontend.ts +54 -5
- package/packages/core/src/cli/dispatch-process.ts +1 -13
- package/packages/core/src/cli/dispatch.ts +392 -75
- package/packages/core/src/cli/harvest.ts +122 -8
- package/packages/core/src/cli/help.ts +62 -9
- package/packages/core/src/cli/init-readiness-remote.ts +110 -0
- package/packages/core/src/cli/init-readiness-shared.ts +72 -0
- package/packages/core/src/cli/init-validation.ts +266 -222
- package/packages/core/src/cli/installation.ts +50 -8
- package/packages/core/src/cli/main.ts +70 -11
- package/packages/core/src/cli/mcp.ts +23 -1
- package/packages/core/src/cli/plugin.ts +7 -4
- package/packages/core/src/cli/remote-poll.ts +277 -0
- package/packages/core/src/cli/repo-state.ts +22 -0
- package/packages/core/src/cli/repository-status.ts +39 -1
- package/packages/core/src/cli/self-update.ts +77 -2
- package/packages/core/src/cli/status.ts +150 -28
- package/packages/core/src/cli/terminals.ts +2 -1
- package/packages/core/src/cli/ticket-assets.ts +247 -0
- package/packages/core/src/cli/ticket.ts +453 -8
- package/packages/core/src/cli/wait.ts +157 -41
- package/packages/core/src/cli/watch.ts +138 -168
- package/packages/core/src/config/live.ts +23 -0
- package/packages/core/src/config/pipeline-source.ts +388 -0
- package/packages/core/src/config/schema.ts +251 -19
- package/packages/core/src/events/catalog.ts +1 -0
- package/packages/core/src/events/globs.ts +47 -0
- package/packages/core/src/events/payloads.ts +52 -2
- package/packages/core/src/events/repository.ts +52 -1
- package/packages/core/src/events/sessions.ts +69 -10
- package/packages/core/src/events/workspace-remote.ts +12 -0
- package/packages/core/src/integration/harness.ts +115 -2
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +1369 -0
- package/packages/core/src/kernel/auto-merge-default.ts +157 -0
- package/packages/core/src/kernel/auto-merge.ts +99 -22
- package/packages/core/src/kernel/dispatch-settings.ts +42 -17
- package/packages/core/src/kernel/dispatch-status.ts +23 -0
- package/packages/core/src/kernel/engine.ts +9 -438
- package/packages/core/src/kernel/generators/build-log.ts +259 -0
- package/packages/core/src/kernel/generators/repository-journals.ts +206 -0
- package/packages/core/src/kernel/generators/session-log.ts +72 -0
- package/packages/core/src/kernel/harvest.ts +62 -18
- package/packages/core/src/kernel/incremental-contract.ts +75 -0
- package/packages/core/src/kernel/incremental-fixtures.ts +211 -0
- package/packages/core/src/kernel/incremental.ts +53 -0
- package/packages/core/src/kernel/log-index.ts +620 -0
- package/packages/core/src/kernel/phases.ts +16 -2
- package/packages/core/src/kernel/pr-attachments.ts +164 -46
- package/packages/core/src/kernel/reducer-registry.ts +164 -0
- package/packages/core/src/kernel/reducer.ts +254 -73
- package/packages/core/src/operator/annotations.ts +6 -0
- package/packages/core/src/operator/control.ts +19 -5
- package/packages/core/src/operator/index.ts +71 -6
- package/packages/core/src/operator/query.ts +253 -24
- package/packages/core/src/operator/read-view.ts +98 -0
- package/packages/core/src/operator/registry.ts +55 -10
- package/packages/core/src/operator/requests.ts +9 -6
- package/packages/core/src/operator/sandbox-host.ts +140 -0
- package/packages/core/src/operator/sandbox.ts +247 -14
- package/packages/core/src/operator/ticket-source-backend.ts +47 -0
- package/packages/core/src/orchestrator/approvals.ts +115 -0
- package/packages/core/src/orchestrator/conversation.ts +167 -0
- package/packages/core/src/orchestrator/model.ts +86 -0
- package/packages/core/src/orchestrator/prompt.ts +61 -0
- package/packages/core/src/orchestrator/tools.ts +117 -0
- package/packages/core/src/orchestrator/turn-runner.ts +457 -0
- package/packages/core/src/plugin-sdk/index.ts +51 -1
- package/packages/core/src/plugins/load.ts +135 -7
- package/packages/core/src/plugins/manifest.ts +183 -10
- package/packages/core/src/plugins/registry.ts +73 -4
- package/packages/core/src/ports/forge/create.ts +1 -1
- package/packages/core/src/ports/forge/fake.ts +31 -0
- package/packages/core/src/ports/forge/github-transport.ts +13 -2
- package/packages/core/src/ports/forge/github.ts +13 -2
- package/packages/core/src/ports/forge/local-git.ts +31 -1
- package/packages/core/src/ports/runner/claude.ts +2 -2
- package/packages/core/src/ports/runner/codex.ts +6 -2
- package/packages/core/src/ports/runner/fake.ts +7 -1
- package/packages/core/src/ports/runner/pi.ts +8 -6
- package/packages/core/src/ports/runner/provider-error.ts +13 -3
- package/packages/core/src/ports/tickets/fake.ts +5 -1
- package/packages/core/src/ports/tickets/file.ts +169 -48
- package/packages/core/src/ports/tickets/{remote-index.ts → hosted-index.ts} +3 -2
- package/packages/core/src/ports/tickets/linear.ts +33 -1
- package/packages/core/src/ports/tickets/operations.ts +1 -1
- package/packages/core/src/ports/tickets/remote.ts +8 -1
- package/packages/core/src/ports/types.ts +49 -3
- package/packages/core/src/ports/workspace/builtin-capabilities.ts +114 -0
- package/packages/core/src/ports/workspace/create.ts +74 -39
- package/packages/core/src/ports/workspace/distribution-archive.ts +171 -12
- package/packages/core/src/ports/workspace/fake.ts +169 -4
- package/packages/core/src/ports/workspace/git-worktree.ts +246 -26
- package/packages/core/src/ports/workspace/github-origin.ts +36 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +7 -1
- package/packages/core/src/ports/workspace/provider-capabilities.ts +171 -0
- package/packages/core/src/ports/workspace/vercel-capabilities.ts +110 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +142 -114
- package/packages/core/src/processes/build-child.ts +17 -2
- package/packages/core/src/processes/build-execution-state.ts +68 -15
- package/packages/core/src/processes/build-runner.ts +238 -93
- package/packages/core/src/processes/dispatcher-selectors.ts +123 -0
- package/packages/core/src/processes/dispatcher.ts +501 -201
- package/packages/core/src/processes/execution-settlement.ts +72 -39
- package/packages/core/src/processes/harvest-child.ts +5 -0
- package/packages/core/src/processes/harvest-execution-state.ts +41 -29
- package/packages/core/src/processes/harvest-runner.ts +66 -19
- package/packages/core/src/processes/harvest.ts +196 -24
- package/packages/core/src/processes/infrastructure-failure-budget.ts +31 -18
- package/packages/core/src/processes/orchestrator-tick.ts +360 -0
- package/packages/core/src/processes/publication-state.ts +164 -52
- package/packages/core/src/processes/repo-view.ts +1436 -0
- package/packages/core/src/processes/sandbox-state.ts +36 -2
- package/packages/core/src/processes/tick-state.ts +33 -0
- package/packages/core/src/store/adapter.ts +41 -0
- package/packages/core/src/store/build-scope.ts +90 -0
- package/packages/core/src/store/contract.ts +1205 -0
- package/packages/core/src/store/digest.ts +102 -0
- package/packages/core/src/store/local/schema.ts +46 -2
- package/packages/core/src/store/local/store.ts +445 -4
- package/packages/core/src/store/memory.ts +246 -0
- package/packages/core/src/store/new-build.ts +17 -0
- package/packages/core/src/store/phase-session.ts +94 -0
- package/packages/core/src/store/remote/client.ts +310 -2
- package/packages/core/src/store/remote/index.ts +14 -2
- package/packages/core/src/store/remote/protocol.ts +115 -12
- package/packages/core/src/store/remote/version.ts +6 -2
- package/packages/core/src/store/repo-state-events.ts +205 -0
- package/packages/core/src/store/retention.ts +7 -0
- package/packages/core/src/store/session-handle.ts +94 -0
- package/packages/core/src/{sessions/reducer.ts → store/session-reducer.ts} +79 -19
- package/packages/core/src/store/snapshots.ts +88 -0
- package/packages/core/src/store/streams/session-writer.ts +42 -2
- package/packages/core/src/store/streams/wait.ts +4 -5
- package/packages/core/src/store/ticket-assets.ts +458 -0
- package/packages/core/src/store/types.ts +160 -1
- package/packages/core/src/testing/index.ts +59 -0
- package/skills/code-review/SKILL.md +5 -1
- package/skills/guide/SKILL.md +152 -40
- package/skills/guide/references/operator-api.md +2 -2
- package/skills/guide/references/plugin-authoring.md +17 -0
- package/skills/guide/references/remote-store-protocol.md +204 -16
- package/skills/guide/references/remote-ticket-protocol.md +1 -1
- package/skills/guide/references/setup.md +12 -9
- package/skills/implement/SKILL.md +3 -0
- package/skills/operate/SKILL.md +126 -0
- package/skills/plan/SKILL.md +4 -0
- package/skills/plan-review/SKILL.md +5 -1
- package/skills/spec/SKILL.md +46 -3
- package/skills/tickets/SKILL.md +57 -5
- package/packages/core/src/cli/dashboard/frame-image.ts +0 -373
- package/packages/core/src/cli/store-opening.contract.ts +0 -289
- package/packages/core/src/cli/testkit.ts +0 -194
- package/packages/core/src/markdown.ts +0 -67
- package/packages/core/src/operator/client.ts +0 -311
- package/packages/core/src/operator/server.ts +0 -616
- package/packages/core/src/operator/sessions.ts +0 -223
- package/packages/core/src/ports/runner/live-contract-fixture.ts +0 -176
- package/packages/core/src/ports/tickets/remote-server.ts +0 -185
- package/packages/core/src/store/remote/server.ts +0 -923
- package/packages/core/src/testing/store-failures.ts +0 -32
|
@@ -1,289 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared behavioral contract for finite sessionless commands that own a
|
|
3
|
-
* BuildStore. Register this against distinct command shells so selection,
|
|
4
|
-
* credentials, production composition, and cleanup cannot drift per command.
|
|
5
|
-
*/
|
|
6
|
-
import { describe, expect, test } from 'bun:test'
|
|
7
|
-
import { existsSync } from 'node:fs'
|
|
8
|
-
import { mkdtemp, rm } from 'node:fs/promises'
|
|
9
|
-
import { tmpdir } from 'node:os'
|
|
10
|
-
import { join } from 'node:path'
|
|
11
|
-
import type { Exec } from '../ports/workspace/git-worktree'
|
|
12
|
-
import { MemoryBuildStore } from '../store/memory'
|
|
13
|
-
import { startStoreServer } from '../store/remote/server'
|
|
14
|
-
import { mintToken } from '../store/remote/token'
|
|
15
|
-
import type { BuildStore } from '../store/types'
|
|
16
|
-
import type { StoreOpener } from './store-opening'
|
|
17
|
-
|
|
18
|
-
const MAIN_REPO = '/main/repo'
|
|
19
|
-
const LINKED_REPO = '/linked/worktree'
|
|
20
|
-
|
|
21
|
-
const mainRepoExec: Exec = async (cmd) =>
|
|
22
|
-
cmd[1] === 'remote'
|
|
23
|
-
? // No origin remote: identity falls back to the resolved checkout path.
|
|
24
|
-
{ stdout: '', stderr: "error: No such remote 'origin'\n", exitCode: 2 }
|
|
25
|
-
: {
|
|
26
|
-
stdout: `${MAIN_REPO}/.git\n${MAIN_REPO}/.git\n${MAIN_REPO}\n`,
|
|
27
|
-
stderr: '',
|
|
28
|
-
exitCode: 0,
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const noGit: Exec = async () => ({
|
|
32
|
-
stdout: '',
|
|
33
|
-
stderr: 'not a git repository',
|
|
34
|
-
exitCode: 128,
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
export interface StoreOpeningCommandOpts {
|
|
38
|
-
targetRepo: string
|
|
39
|
-
env: Record<string, string | undefined>
|
|
40
|
-
exec: Exec
|
|
41
|
-
stdout: (line: string) => void
|
|
42
|
-
storeRef?: string
|
|
43
|
-
openStore?: StoreOpener
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface StoreOpeningCommandAdapter {
|
|
47
|
-
run: (opts: StoreOpeningCommandOpts) => Promise<void>
|
|
48
|
-
/** Extract a command-specific marker proving it used canonical main-repo
|
|
49
|
-
* identity (for example a filtered build slug or a projected repo path). */
|
|
50
|
-
canonicalMarker: (stdout: string[]) => string | undefined
|
|
51
|
-
expectedCanonicalMarker: string
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function trackedStore(
|
|
55
|
-
store: MemoryBuildStore,
|
|
56
|
-
opts: { failOperations?: boolean; onClose: () => void },
|
|
57
|
-
): BuildStore {
|
|
58
|
-
return new Proxy(store, {
|
|
59
|
-
get(target, prop) {
|
|
60
|
-
if (prop === 'close') {
|
|
61
|
-
return async () => opts.onClose()
|
|
62
|
-
}
|
|
63
|
-
const value = Reflect.get(target, prop, target) as unknown
|
|
64
|
-
if (typeof value !== 'function') return value
|
|
65
|
-
if (opts.failOperations === true) {
|
|
66
|
-
return async () => {
|
|
67
|
-
throw new Error('store operation failed')
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return value.bind(target) as unknown
|
|
71
|
-
},
|
|
72
|
-
}) as unknown as BuildStore
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function capture(
|
|
76
|
-
adapter: StoreOpeningCommandAdapter,
|
|
77
|
-
opts: Omit<StoreOpeningCommandOpts, 'stdout'>,
|
|
78
|
-
): Promise<string[]> {
|
|
79
|
-
const stdout: string[] = []
|
|
80
|
-
await adapter.run({ ...opts, stdout: (line) => stdout.push(line) })
|
|
81
|
-
return stdout
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function describeStoreOpeningContract(
|
|
85
|
-
name: string,
|
|
86
|
-
adapter: StoreOpeningCommandAdapter,
|
|
87
|
-
): void {
|
|
88
|
-
describe(`sessionless store-opening contract: ${name}`, () => {
|
|
89
|
-
test('shares reference precedence, normalization, URL preservation, and opaque token forwarding', async () => {
|
|
90
|
-
const store = new MemoryBuildStore()
|
|
91
|
-
const opened: Array<{ ref: string; token?: string }> = []
|
|
92
|
-
const openStore: StoreOpener = (ref, token) => {
|
|
93
|
-
opened.push({ ref, ...(token !== undefined ? { token } : {}) })
|
|
94
|
-
return store
|
|
95
|
-
}
|
|
96
|
-
const base = {
|
|
97
|
-
targetRepo: LINKED_REPO,
|
|
98
|
-
exec: mainRepoExec,
|
|
99
|
-
openStore,
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
await capture(adapter, {
|
|
103
|
-
...base,
|
|
104
|
-
env: { AB_STORE: 'environment' },
|
|
105
|
-
storeRef: 'flag/../selected',
|
|
106
|
-
})
|
|
107
|
-
await capture(adapter, {
|
|
108
|
-
...base,
|
|
109
|
-
env: { AB_STORE: 'environment/../selected-env' },
|
|
110
|
-
})
|
|
111
|
-
await capture(adapter, {
|
|
112
|
-
...base,
|
|
113
|
-
env: { AB_STORE: '/absolute/state' },
|
|
114
|
-
storeRef: ' ',
|
|
115
|
-
})
|
|
116
|
-
await capture(adapter, {
|
|
117
|
-
...base,
|
|
118
|
-
env: { AB_STORE: ' \t' },
|
|
119
|
-
storeRef: '',
|
|
120
|
-
})
|
|
121
|
-
await capture(adapter, {
|
|
122
|
-
...base,
|
|
123
|
-
env: {},
|
|
124
|
-
storeRef: 'http://store.example.test/api',
|
|
125
|
-
})
|
|
126
|
-
await capture(adapter, {
|
|
127
|
-
...base,
|
|
128
|
-
env: {},
|
|
129
|
-
storeRef: 'https://store.example.test/api?x=1',
|
|
130
|
-
})
|
|
131
|
-
await capture(adapter, {
|
|
132
|
-
...base,
|
|
133
|
-
env: { AB_TOKEN: '' },
|
|
134
|
-
storeRef: 'https://store.example.test/empty-token',
|
|
135
|
-
})
|
|
136
|
-
await capture(adapter, {
|
|
137
|
-
...base,
|
|
138
|
-
env: { AB_TOKEN: ' opaque token ' },
|
|
139
|
-
storeRef: 'https://store.example.test/opaque-token',
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
expect(opened).toEqual([
|
|
143
|
-
{ ref: `${MAIN_REPO}/selected` },
|
|
144
|
-
{ ref: `${MAIN_REPO}/selected-env` },
|
|
145
|
-
{ ref: '/absolute/state' },
|
|
146
|
-
{ ref: `${MAIN_REPO}/.autobuild` },
|
|
147
|
-
{ ref: 'http://store.example.test/api' },
|
|
148
|
-
{ ref: 'https://store.example.test/api?x=1' },
|
|
149
|
-
{ ref: 'https://store.example.test/empty-token' },
|
|
150
|
-
{
|
|
151
|
-
ref: 'https://store.example.test/opaque-token',
|
|
152
|
-
token: ' opaque token ',
|
|
153
|
-
},
|
|
154
|
-
])
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
test('uses canonical main-repository identity', async () => {
|
|
158
|
-
const store = new MemoryBuildStore()
|
|
159
|
-
await store.createBuild({ slug: 'main-build', repo: MAIN_REPO })
|
|
160
|
-
await store.createBuild({ slug: 'other-build', repo: '/other/repo' })
|
|
161
|
-
await store.ensureRepo(MAIN_REPO)
|
|
162
|
-
const stdout = await capture(adapter, {
|
|
163
|
-
targetRepo: LINKED_REPO,
|
|
164
|
-
env: {},
|
|
165
|
-
exec: mainRepoExec,
|
|
166
|
-
openStore: () => store,
|
|
167
|
-
})
|
|
168
|
-
expect(adapter.canonicalMarker(stdout)).toBe(adapter.expectedCanonicalMarker)
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
test('closes exactly once on success, store failure, and output failure', async () => {
|
|
172
|
-
const run = async (mode: 'success' | 'store' | 'output'): Promise<number> => {
|
|
173
|
-
const store = new MemoryBuildStore()
|
|
174
|
-
let closes = 0
|
|
175
|
-
const tracked = trackedStore(store, {
|
|
176
|
-
failOperations: mode === 'store',
|
|
177
|
-
onClose: () => {
|
|
178
|
-
closes += 1
|
|
179
|
-
},
|
|
180
|
-
})
|
|
181
|
-
const command = adapter.run({
|
|
182
|
-
targetRepo: LINKED_REPO,
|
|
183
|
-
env: {},
|
|
184
|
-
exec: mainRepoExec,
|
|
185
|
-
openStore: () => tracked,
|
|
186
|
-
stdout:
|
|
187
|
-
mode === 'output'
|
|
188
|
-
? () => {
|
|
189
|
-
throw new Error('output failed')
|
|
190
|
-
}
|
|
191
|
-
: () => {},
|
|
192
|
-
})
|
|
193
|
-
if (mode === 'success') await command
|
|
194
|
-
else {
|
|
195
|
-
await expect(command).rejects.toThrow(
|
|
196
|
-
mode === 'store' ? 'store operation failed' : 'output failed',
|
|
197
|
-
)
|
|
198
|
-
}
|
|
199
|
-
return closes
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
expect(await run('success')).toBe(1)
|
|
203
|
-
expect(await run('store')).toBe(1)
|
|
204
|
-
expect(await run('output')).toBe(1)
|
|
205
|
-
})
|
|
206
|
-
|
|
207
|
-
test('propagates opener failure without phantom cleanup', async () => {
|
|
208
|
-
let closes = 0
|
|
209
|
-
const unopened = new MemoryBuildStore()
|
|
210
|
-
unopened.close = async () => {
|
|
211
|
-
closes += 1
|
|
212
|
-
}
|
|
213
|
-
await expect(
|
|
214
|
-
adapter.run({
|
|
215
|
-
targetRepo: LINKED_REPO,
|
|
216
|
-
env: {},
|
|
217
|
-
exec: mainRepoExec,
|
|
218
|
-
stdout: () => {},
|
|
219
|
-
openStore: () => {
|
|
220
|
-
throw new Error('open failed')
|
|
221
|
-
},
|
|
222
|
-
}),
|
|
223
|
-
).rejects.toThrow('open failed')
|
|
224
|
-
expect(closes).toBe(0)
|
|
225
|
-
})
|
|
226
|
-
|
|
227
|
-
test('production composition opens a local SQLite root', async () => {
|
|
228
|
-
const repo = await mkdtemp(join(tmpdir(), 'ab-store-opening-local-'))
|
|
229
|
-
try {
|
|
230
|
-
await adapter.run({
|
|
231
|
-
targetRepo: repo,
|
|
232
|
-
env: {},
|
|
233
|
-
exec: noGit,
|
|
234
|
-
stdout: () => {},
|
|
235
|
-
storeRef: 'state',
|
|
236
|
-
})
|
|
237
|
-
expect(existsSync(join(repo, 'state', 'autobuild.sqlite'))).toBe(true)
|
|
238
|
-
} finally {
|
|
239
|
-
await rm(repo, { recursive: true, force: true })
|
|
240
|
-
}
|
|
241
|
-
})
|
|
242
|
-
|
|
243
|
-
test('production composition forwards a token to remote HTTP and rejects missing or invalid credentials', async () => {
|
|
244
|
-
const secret = 'store-opening-secret'
|
|
245
|
-
const now = new Date('2026-07-15T12:00:00.000Z')
|
|
246
|
-
const backing = new MemoryBuildStore({ clock: () => now })
|
|
247
|
-
const server = startStoreServer({
|
|
248
|
-
store: backing,
|
|
249
|
-
secret,
|
|
250
|
-
clock: () => now,
|
|
251
|
-
})
|
|
252
|
-
const token = mintToken(secret, {
|
|
253
|
-
build: '*',
|
|
254
|
-
session: '*',
|
|
255
|
-
exp: now.getTime() + 60_000,
|
|
256
|
-
})
|
|
257
|
-
try {
|
|
258
|
-
await adapter.run({
|
|
259
|
-
targetRepo: LINKED_REPO,
|
|
260
|
-
env: { AB_TOKEN: token },
|
|
261
|
-
exec: mainRepoExec,
|
|
262
|
-
stdout: () => {},
|
|
263
|
-
storeRef: server.url,
|
|
264
|
-
})
|
|
265
|
-
await expect(
|
|
266
|
-
adapter.run({
|
|
267
|
-
targetRepo: LINKED_REPO,
|
|
268
|
-
env: {},
|
|
269
|
-
exec: mainRepoExec,
|
|
270
|
-
stdout: () => {},
|
|
271
|
-
storeRef: server.url,
|
|
272
|
-
}),
|
|
273
|
-
).rejects.toThrow(/missing bearer token/)
|
|
274
|
-
await expect(
|
|
275
|
-
adapter.run({
|
|
276
|
-
targetRepo: LINKED_REPO,
|
|
277
|
-
env: { AB_TOKEN: 'invalid-token' },
|
|
278
|
-
exec: mainRepoExec,
|
|
279
|
-
stdout: () => {},
|
|
280
|
-
storeRef: server.url,
|
|
281
|
-
}),
|
|
282
|
-
).rejects.toThrow(/invalid or expired token/)
|
|
283
|
-
} finally {
|
|
284
|
-
await server.stop()
|
|
285
|
-
await backing.close()
|
|
286
|
-
}
|
|
287
|
-
})
|
|
288
|
-
})
|
|
289
|
-
}
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Test-support fixtures for the CLI suite — imported only by the colocated
|
|
3
|
-
* `*.test.ts` files, never by production code (mirrors the src/testing rule).
|
|
4
|
-
*
|
|
5
|
-
* The store fixture is a seeded MemoryBuildStore (the reference adapter — its
|
|
6
|
-
* behavior IS the contract, store/contract.ts) and the git fixtures are real
|
|
7
|
-
* throwaway repos, matching the patterns in git-worktree.test.ts.
|
|
8
|
-
*/
|
|
9
|
-
import { mkdir, writeFile } from 'node:fs/promises'
|
|
10
|
-
import { join } from 'node:path'
|
|
11
|
-
import type { EventEnvelope, EventWrite } from '../events/catalog'
|
|
12
|
-
import { DISPATCHER } from '../events/envelope'
|
|
13
|
-
import type { EventType } from '../events/payloads'
|
|
14
|
-
import type { PrImageHostTarget } from '../ontology'
|
|
15
|
-
import { sequentialIds } from '../ids'
|
|
16
|
-
import { FakeForge } from '../ports/forge/fake'
|
|
17
|
-
import { spawnExec } from '../ports/workspace/git-worktree'
|
|
18
|
-
import { MemoryBuildStore } from '../store/memory'
|
|
19
|
-
import type { BuildStore } from '../store/types'
|
|
20
|
-
import { steppingClock } from '../testing/fixed'
|
|
21
|
-
import type { CliEnv } from './env'
|
|
22
|
-
import type { CliDeps } from './main'
|
|
23
|
-
|
|
24
|
-
export const BUILD = 'auth-rate-limit'
|
|
25
|
-
export const BRANCH = 'ab/auth-rate-limit'
|
|
26
|
-
|
|
27
|
-
export function makeEnv(overrides: Partial<CliEnv> = {}): CliEnv {
|
|
28
|
-
return {
|
|
29
|
-
store: '/tmp/ab-store',
|
|
30
|
-
build: BUILD,
|
|
31
|
-
phase: 'implement',
|
|
32
|
-
round: 1,
|
|
33
|
-
session: 's_test',
|
|
34
|
-
...overrides,
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Build + `build.created` + spec@0 (`spec.imported`) — every phase's floor. */
|
|
39
|
-
export async function seedStore(
|
|
40
|
-
opts: { imageHost?: PrImageHostTarget } = {},
|
|
41
|
-
): Promise<MemoryBuildStore> {
|
|
42
|
-
const store = new MemoryBuildStore({ clock: steppingClock() })
|
|
43
|
-
await store.createBuild({ slug: BUILD, repo: 'acme/app', branch: BRANCH })
|
|
44
|
-
await store.append(BUILD, {
|
|
45
|
-
actor: DISPATCHER,
|
|
46
|
-
type: 'build.created',
|
|
47
|
-
payload: {
|
|
48
|
-
ticket: {
|
|
49
|
-
source: 'linear',
|
|
50
|
-
id: 'ENG-42',
|
|
51
|
-
url: 'https://linear.app/acme/issue/ENG-42',
|
|
52
|
-
title: 'Auth rate limiting',
|
|
53
|
-
},
|
|
54
|
-
repo: 'acme/app',
|
|
55
|
-
baseBranch: 'main',
|
|
56
|
-
...(opts.imageHost !== undefined ? { pr: { imageHost: opts.imageHost } } : {}),
|
|
57
|
-
},
|
|
58
|
-
})
|
|
59
|
-
await store.putArtifact(BUILD, {
|
|
60
|
-
kind: 'spec',
|
|
61
|
-
content: '# Spec: auth rate limiting\n',
|
|
62
|
-
})
|
|
63
|
-
await store.append(BUILD, {
|
|
64
|
-
actor: DISPATCHER,
|
|
65
|
-
type: 'spec.imported',
|
|
66
|
-
payload: {
|
|
67
|
-
artifact: { kind: 'spec', rev: 0 },
|
|
68
|
-
ticket: { source: 'linear', id: 'ENG-42' },
|
|
69
|
-
},
|
|
70
|
-
})
|
|
71
|
-
return store
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export interface TestDeps extends CliDeps {
|
|
75
|
-
forge: FakeForge
|
|
76
|
-
out: string[]
|
|
77
|
-
err: string[]
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function makeDeps(opts: {
|
|
81
|
-
store: MemoryBuildStore
|
|
82
|
-
env: CliEnv
|
|
83
|
-
workspacePath?: string
|
|
84
|
-
forge?: FakeForge
|
|
85
|
-
}): TestDeps {
|
|
86
|
-
const out: string[] = []
|
|
87
|
-
const err: string[] = []
|
|
88
|
-
const forge = opts.forge ?? new FakeForge()
|
|
89
|
-
return {
|
|
90
|
-
store: opts.store,
|
|
91
|
-
env: opts.env,
|
|
92
|
-
workspacePath: opts.workspacePath ?? '/nonexistent-workspace',
|
|
93
|
-
forge,
|
|
94
|
-
exec: spawnExec,
|
|
95
|
-
ids: sequentialIds(),
|
|
96
|
-
clock: steppingClock(),
|
|
97
|
-
stdout: (line) => out.push(line),
|
|
98
|
-
stderr: (line) => err.push(line),
|
|
99
|
-
out,
|
|
100
|
-
err,
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Wrap a store so ONE build's compare-and-set append fails outright, to drive
|
|
106
|
-
* the bulk walk's partial-progress path. Prototype-proxy idiom, as
|
|
107
|
-
* `withRacingAppend` in bulk-control.test.ts; every other build and every other
|
|
108
|
-
* operation delegates untouched, so the walk's real bookkeeping is what the
|
|
109
|
-
* failure report is built from.
|
|
110
|
-
*/
|
|
111
|
-
export function withFailingAppend(store: MemoryBuildStore, slug: string, error: Error): BuildStore {
|
|
112
|
-
const proxy = Object.create(store) as MemoryBuildStore
|
|
113
|
-
proxy.appendIfCurrent = async <T extends EventType>(
|
|
114
|
-
target: string,
|
|
115
|
-
expectedSeq: number,
|
|
116
|
-
event: EventWrite<T>,
|
|
117
|
-
): Promise<EventEnvelope<T> | null> => {
|
|
118
|
-
if (target === slug) throw error
|
|
119
|
-
return store.appendIfCurrent(target, expectedSeq, event)
|
|
120
|
-
}
|
|
121
|
-
return proxy
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// ── Real-git fixtures (identity pinned per invocation, like git-worktree.test) ─
|
|
125
|
-
|
|
126
|
-
export const GIT_ID = [
|
|
127
|
-
'-c',
|
|
128
|
-
'user.email=ab@test.invalid',
|
|
129
|
-
'-c',
|
|
130
|
-
'user.name=ab-test',
|
|
131
|
-
'-c',
|
|
132
|
-
'commit.gpgsign=false',
|
|
133
|
-
]
|
|
134
|
-
|
|
135
|
-
export async function runGit(args: string[], cwd: string): Promise<string> {
|
|
136
|
-
const result = await spawnExec(['git', ...args], { cwd })
|
|
137
|
-
if (result.exitCode !== 0) {
|
|
138
|
-
throw new Error(`git ${args.join(' ')} failed: ${result.stderr}`)
|
|
139
|
-
}
|
|
140
|
-
return result.stdout.trim()
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* A workspace repo on branch `ab/auth-rate-limit`, branched off `main`.
|
|
145
|
-
* Deliberately does NOT gitignore `.ab/` — real repos don't either (§7:
|
|
146
|
-
* establishing the gitignored scratch dir is the PRODUCT's job, via the
|
|
147
|
-
* self-excluding `.ab/.gitignore` that `ab context` writes); a fixture that
|
|
148
|
-
* hand-wrote the ignore would hide a regression in that mechanism.
|
|
149
|
-
*/
|
|
150
|
-
export async function initWorkspaceRepo(dir: string, branch = BRANCH): Promise<void> {
|
|
151
|
-
await mkdir(dir, { recursive: true })
|
|
152
|
-
await runGit(['init', '-q', '-b', 'main'], dir)
|
|
153
|
-
await writeFile(join(dir, 'README.md'), 'fixture\n')
|
|
154
|
-
await runGit(['add', '.'], dir)
|
|
155
|
-
await runGit([...GIT_ID, 'commit', '-q', '-m', 'initial'], dir)
|
|
156
|
-
await runGit(['checkout', '-q', '-b', branch], dir)
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Attach a throwaway bare origin, seed its build ref at the local branch tip,
|
|
161
|
-
* and seed `main` at an independently selected target commit. Directly moving
|
|
162
|
-
* the bare target ref deliberately leaves operator-owned local/tracking refs
|
|
163
|
-
* untouched, matching the real workspace boundary tests.
|
|
164
|
-
*/
|
|
165
|
-
export async function initBareOrigin(
|
|
166
|
-
workspace: string,
|
|
167
|
-
remote: string,
|
|
168
|
-
branch = BRANCH,
|
|
169
|
-
target = 'HEAD',
|
|
170
|
-
): Promise<void> {
|
|
171
|
-
const targetSha = await runGit(['rev-parse', '--verify', `${target}^{commit}`], workspace)
|
|
172
|
-
await mkdir(remote, { recursive: true })
|
|
173
|
-
await runGit(['init', '-q', '--bare'], remote)
|
|
174
|
-
await runGit(['remote', 'add', 'origin', remote], workspace)
|
|
175
|
-
await runGit(['push', '-q', 'origin', `${branch}:refs/heads/${branch}`], workspace)
|
|
176
|
-
await runGit(['update-ref', 'refs/heads/main', targetSha], remote)
|
|
177
|
-
await runGit(['symbolic-ref', 'HEAD', 'refs/heads/main'], remote)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
export function remoteBranchHead(remote: string, branch = BRANCH): Promise<string> {
|
|
181
|
-
return runGit(['rev-parse', `refs/heads/${branch}`], remote)
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
export async function commitFile(
|
|
185
|
-
dir: string,
|
|
186
|
-
file: string,
|
|
187
|
-
content: string,
|
|
188
|
-
message: string,
|
|
189
|
-
): Promise<string> {
|
|
190
|
-
await writeFile(join(dir, file), content)
|
|
191
|
-
await runGit(['add', file], dir)
|
|
192
|
-
await runGit([...GIT_ID, 'commit', '-q', '-m', message], dir)
|
|
193
|
-
return runGit(['rev-parse', 'HEAD'], dir)
|
|
194
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Approximate Markdown reference extraction, shared by the checks that ask
|
|
3
|
-
* "does this link point at something that exists?".
|
|
4
|
-
*
|
|
5
|
-
* These are regexes, not a CommonMark parser. Reference-style targets
|
|
6
|
-
* (`![alt][ref]`) and targets containing parentheses are invisible to them.
|
|
7
|
-
* Both callers are link-existence checks, where an unseen reference fails
|
|
8
|
-
* toward a loud complaint rather than a silent pass, so the approximation is
|
|
9
|
-
* safe — but a false report is a signal to widen the parser, never to delete
|
|
10
|
-
* the thing it failed to see.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Blanks out fenced code blocks, preserving line count. A target inside a fence
|
|
15
|
-
* is sample text, not a rendered reference. Backtick and tilde fences are
|
|
16
|
-
* tracked separately so a `~~~` line inside a ``` block does not close it.
|
|
17
|
-
*/
|
|
18
|
-
export function withoutFencedCode(markdown: string): string {
|
|
19
|
-
const lines = markdown.split('\n')
|
|
20
|
-
let fence: '`' | '~' | undefined
|
|
21
|
-
return lines
|
|
22
|
-
.map((line) => {
|
|
23
|
-
const opening = line.match(/^\s*(`{3,}|~{3,})/)
|
|
24
|
-
if (opening) {
|
|
25
|
-
const marker = opening[1]![0] as '`' | '~'
|
|
26
|
-
if (fence === undefined) fence = marker
|
|
27
|
-
else if (fence === marker) fence = undefined
|
|
28
|
-
return ''
|
|
29
|
-
}
|
|
30
|
-
return fence === undefined ? line : ''
|
|
31
|
-
})
|
|
32
|
-
.join('\n')
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Inline link and image targets, in source order. Angle brackets are unwrapped
|
|
37
|
-
* and a title suffix is dropped, so `[a](<b.md> "T")` yields `b.md`.
|
|
38
|
-
*/
|
|
39
|
-
export function markdownTargets(markdown: string): string[] {
|
|
40
|
-
const targets: string[] = []
|
|
41
|
-
for (const match of markdown.matchAll(/!?\[[^\]]*\]\(([^)]+)\)/g)) {
|
|
42
|
-
const inline = match[1]!.trim()
|
|
43
|
-
// An angle-bracketed destination runs to its closing `>`, and whatever
|
|
44
|
-
// follows is the title. The two must be read together: unwrapping first
|
|
45
|
-
// fails whenever a title is present (the text no longer ends in `>`), and
|
|
46
|
-
// splitting the title off first would truncate a bracketed destination that
|
|
47
|
-
// legitimately contains whitespace and a quote. A `<` with no closing `>`
|
|
48
|
-
// is malformed, and falls through to the unbracketed reading.
|
|
49
|
-
const closing = inline.startsWith('<') ? inline.indexOf('>') : -1
|
|
50
|
-
targets.push(closing === -1 ? inline.split(/\s+["']/u, 1)[0]! : inline.slice(1, closing))
|
|
51
|
-
}
|
|
52
|
-
return targets
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* `src` attributes of raw `<img>` tags, in source order. Kept separate from
|
|
57
|
-
* `markdownTargets` so callers that only care about Markdown syntax keep their
|
|
58
|
-
* existing behaviour: Markdown permits raw HTML, and this repository's README
|
|
59
|
-
* already mixes it in, so a centred hero image would otherwise be invisible.
|
|
60
|
-
*/
|
|
61
|
-
export function htmlImageTargets(markdown: string): string[] {
|
|
62
|
-
const targets: string[] = []
|
|
63
|
-
for (const match of markdown.matchAll(/<img\b[^>]*?\ssrc\s*=\s*("([^"]*)"|'([^']*)')/giu)) {
|
|
64
|
-
targets.push((match[2] ?? match[3])!.trim())
|
|
65
|
-
}
|
|
66
|
-
return targets
|
|
67
|
-
}
|