@stream44.studio/t44-blockchaincommons.com 0.1.0-rc.2

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.
Files changed (37) hide show
  1. package/.dco-signatures +9 -0
  2. package/.github/workflows/dco.yml +12 -0
  3. package/.github/workflows/gordian-open-integrity.yml +13 -0
  4. package/.o/GordianOpenIntegrity-CurrentLifehash.svg +1026 -0
  5. package/.o/GordianOpenIntegrity-InceptionLifehash.svg +1026 -0
  6. package/.o/GordianOpenIntegrity.yaml +25 -0
  7. package/DCO.md +34 -0
  8. package/README.md +210 -0
  9. package/action.yml +47 -0
  10. package/bin/oi +152 -0
  11. package/caps/GordianOpenIntegrity.test.ts +879 -0
  12. package/caps/GordianOpenIntegrity.ts +821 -0
  13. package/caps/XidDocumentLedger.test.ts +687 -0
  14. package/caps/XidDocumentLedger.ts +545 -0
  15. package/caps/__snapshots__/XidDocumentLedger.test.ts.snap +11 -0
  16. package/caps/__snapshots__/XidLedger.test.ts.snap +11 -0
  17. package/caps/lifehash.test.ts +302 -0
  18. package/caps/lifehash.ts +142 -0
  19. package/caps/open-integrity-js.test.ts +252 -0
  20. package/caps/open-integrity-js.ts +485 -0
  21. package/caps/open-integrity-sh.test.ts +188 -0
  22. package/caps/open-integrity-sh.ts +187 -0
  23. package/caps/open-integrity.test.ts +259 -0
  24. package/caps/provenance-mark-cli.test.ts +387 -0
  25. package/caps/provenance-mark-cli.ts +174 -0
  26. package/caps/provenance-mark.test.ts +233 -0
  27. package/caps/provenance-mark.ts +223 -0
  28. package/caps/xid.test.ts +828 -0
  29. package/caps/xid.ts +565 -0
  30. package/examples/01-XID-DocumentLedger/__snapshots__/main.test.ts.snap +10 -0
  31. package/examples/01-XID-DocumentLedger/main.test.ts +182 -0
  32. package/examples/02-XID-Rotate-InceptionKey/__snapshots__/main.test.ts.snap +53 -0
  33. package/examples/02-XID-Rotate-InceptionKey/main.test.ts +232 -0
  34. package/examples/03-GordianOpenIntegrity/main.test.ts +176 -0
  35. package/examples/04-GordianOpenIntegrityCli/main.test.ts +119 -0
  36. package/package.json +37 -0
  37. package/tsconfig.json +28 -0
@@ -0,0 +1,485 @@
1
+
2
+
3
+ import { join, dirname } from 'path'
4
+ import { mkdir, writeFile, readFile, rm, access } from 'fs/promises'
5
+ import { tmpdir } from 'os'
6
+
7
+ type AllowedSigner = { email: string; publicKey: string }
8
+
9
+ async function withAllowedSigners<T>(signers: AllowedSigner[], fn: (gitArgs: string[]) => Promise<T>): Promise<T> {
10
+ const lines = signers.map(s => `${s.email} namespaces="git" ${s.publicKey}`)
11
+ const tmpFile = join(tmpdir(), `oi-allowed-signers-${Date.now()}-${Math.random().toString(36).slice(2)}`)
12
+ await writeFile(tmpFile, lines.join('\n') + '\n')
13
+ try {
14
+ return await fn(['-c', `gpg.ssh.allowedSignersFile=${tmpFile}`])
15
+ } finally {
16
+ await rm(tmpFile, { force: true })
17
+ }
18
+ }
19
+
20
+
21
+ async function exec(cmd: string[], options: { cwd: string; env?: Record<string, string> }): Promise<{ stdout: string; stderr: string; exitCode: number }> {
22
+ const proc = Bun.spawn(cmd, {
23
+ cwd: options.cwd,
24
+ env: { ...process.env, ...options.env },
25
+ stdout: 'pipe',
26
+ stderr: 'pipe',
27
+ })
28
+ const stdout = await new Response(proc.stdout).text()
29
+ const stderr = await new Response(proc.stderr).text()
30
+ const exitCode = await proc.exited
31
+ return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode }
32
+ }
33
+
34
+ async function git(args: string[], options: { cwd: string; env?: Record<string, string> }): Promise<{ stdout: string; stderr: string; exitCode: number }> {
35
+ return exec(['git', ...args], options)
36
+ }
37
+
38
+ async function sshKeygen(args: string[]): Promise<{ stdout: string; stderr: string; exitCode: number }> {
39
+ return exec(['ssh-keygen', ...args], { cwd: '/tmp' })
40
+ }
41
+
42
+
43
+ export async function capsule({
44
+ encapsulate,
45
+ CapsulePropertyTypes,
46
+ makeImportStack
47
+ }: {
48
+ encapsulate: any
49
+ CapsulePropertyTypes: any
50
+ makeImportStack: any
51
+ }) {
52
+
53
+ return encapsulate({
54
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
55
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
56
+ '#': {
57
+
58
+ readSigningKey: {
59
+ type: CapsulePropertyTypes.Function,
60
+ value: async function (this: any, context: {
61
+ privateKeyPath: string
62
+ }) {
63
+ const keyPath = context.privateKeyPath
64
+ const publicKey = (await readFile(`${keyPath}.pub`, 'utf-8')).trim()
65
+
66
+ const fpResult = await sshKeygen(['-E', 'sha256', '-lf', keyPath])
67
+ if (fpResult.exitCode !== 0) {
68
+ throw new Error(`ssh-keygen fingerprint failed: ${fpResult.stderr}`)
69
+ }
70
+ const fingerprint = fpResult.stdout.split(' ')[1] || ''
71
+
72
+ return {
73
+ privateKeyPath: keyPath,
74
+ publicKeyPath: `${keyPath}.pub`,
75
+ publicKey,
76
+ fingerprint,
77
+ }
78
+ }
79
+ },
80
+
81
+ generateSigningKey: {
82
+ type: CapsulePropertyTypes.Function,
83
+ value: async function (this: any, context: {
84
+ keyDir: string
85
+ keyName?: string
86
+ passphrase?: string
87
+ }) {
88
+ const keyName = context.keyName || 'sign_id_ed25519'
89
+ const keyPath = join(context.keyDir, keyName)
90
+ const passphrase = context.passphrase || ''
91
+
92
+ await mkdir(context.keyDir, { recursive: true })
93
+
94
+ const result = await sshKeygen([
95
+ '-t', 'ed25519',
96
+ '-f', keyPath,
97
+ '-N', passphrase,
98
+ '-C', keyName,
99
+ ])
100
+
101
+ if (result.exitCode !== 0) {
102
+ throw new Error(`ssh-keygen failed: ${result.stderr}`)
103
+ }
104
+
105
+ const publicKey = await readFile(`${keyPath}.pub`, 'utf-8')
106
+
107
+ // Get fingerprint
108
+ const fpResult = await sshKeygen(['-E', 'sha256', '-lf', keyPath])
109
+ const fingerprint = fpResult.stdout.split(' ')[1] || ''
110
+
111
+ return {
112
+ privateKeyPath: keyPath,
113
+ publicKeyPath: `${keyPath}.pub`,
114
+ publicKey: publicKey.trim(),
115
+ fingerprint,
116
+ }
117
+ }
118
+ },
119
+
120
+ createInceptionCommit: {
121
+ type: CapsulePropertyTypes.Function,
122
+ value: async function (this: any, context: {
123
+ repoDir: string
124
+ signingKeyPath: string
125
+ authorName: string
126
+ authorEmail: string
127
+ message?: string
128
+ contract?: string
129
+ }) {
130
+ // Init repo
131
+ await mkdir(context.repoDir, { recursive: true })
132
+ const initResult = await git(['init'], { cwd: context.repoDir })
133
+ if (initResult.exitCode !== 0) {
134
+ throw new Error(`git init failed: ${initResult.stderr}`)
135
+ }
136
+
137
+ // Get key fingerprint for committer name
138
+ const fpResult = await sshKeygen(['-E', 'sha256', '-lf', context.signingKeyPath])
139
+ if (fpResult.exitCode !== 0) {
140
+ throw new Error(`ssh-keygen fingerprint failed: ${fpResult.stderr}`)
141
+ }
142
+ const fingerprint = fpResult.stdout.split(' ')[1] || ''
143
+
144
+ const dateStr = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z')
145
+
146
+ const env: Record<string, string> = {
147
+ GIT_AUTHOR_NAME: context.authorName,
148
+ GIT_AUTHOR_EMAIL: context.authorEmail,
149
+ GIT_COMMITTER_NAME: fingerprint,
150
+ GIT_COMMITTER_EMAIL: context.authorEmail,
151
+ GIT_AUTHOR_DATE: dateStr,
152
+ GIT_COMMITTER_DATE: dateStr,
153
+ }
154
+
155
+ const inceptionMessage = context.message || '[GordianOpenIntegrity] Establish a SHA-1 root of trust for origin and future commit verification.'
156
+ const ricardianContract = context.contract || 'Trust established using https://github.com/Stream44/t44-BlockchainCommons.com'
157
+
158
+ const commitResult = await git([
159
+ '-c', 'gpg.format=ssh',
160
+ '-c', `user.signingkey=${context.signingKeyPath}`,
161
+ 'commit',
162
+ '--allow-empty',
163
+ '--no-edit',
164
+ '--gpg-sign',
165
+ '-m', inceptionMessage,
166
+ '-m', `Signed-off-by: ${context.authorName} <${context.authorEmail}>`,
167
+ '-m', ricardianContract,
168
+ ], { cwd: context.repoDir, env })
169
+
170
+ if (commitResult.exitCode !== 0) {
171
+ throw new Error(`Inception commit failed: ${commitResult.stderr}`)
172
+ }
173
+
174
+ // Get the commit hash
175
+ const hashResult = await git(['rev-list', '--max-parents=0', 'HEAD'], { cwd: context.repoDir })
176
+ const commitHash = hashResult.stdout.trim()
177
+
178
+ return {
179
+ commitHash,
180
+ did: `did:repo:${commitHash}`,
181
+ fingerprint,
182
+ message: inceptionMessage,
183
+ }
184
+ }
185
+ },
186
+
187
+ getRepoDid: {
188
+ type: CapsulePropertyTypes.Function,
189
+ value: async function (this: any, context: { repoDir: string }) {
190
+ const hashResult = await git(['rev-list', '--max-parents=0', 'HEAD'], { cwd: context.repoDir })
191
+ if (hashResult.exitCode !== 0) {
192
+ throw new Error(`Failed to get first commit: ${hashResult.stderr}`)
193
+ }
194
+ const commitHash = hashResult.stdout.split('\n')[0].trim()
195
+ return `did:repo:${commitHash}`
196
+ }
197
+ },
198
+
199
+ getInceptionCommit: {
200
+ type: CapsulePropertyTypes.Function,
201
+ value: async function (this: any, context: { repoDir: string }) {
202
+ const hashResult = await git(['rev-list', '--max-parents=0', 'HEAD'], { cwd: context.repoDir })
203
+ if (hashResult.exitCode !== 0) {
204
+ throw new Error(`Failed to get inception commit: ${hashResult.stderr}`)
205
+ }
206
+ const commitHash = hashResult.stdout.split('\n')[0].trim()
207
+
208
+ // Get full details
209
+ const showResult = await git(['show', '--pretty=fuller', commitHash], { cwd: context.repoDir })
210
+
211
+ // Get committer details
212
+ const committerResult = await git(['log', '--format=%cn <%ce>', '-1', commitHash], { cwd: context.repoDir })
213
+
214
+ // Get key fingerprint
215
+ const keyResult = await git(['log', '--format=%GK', '-1', commitHash], { cwd: context.repoDir })
216
+
217
+ // Get signature status
218
+ const sigStatusResult = await git(['log', '--format=%G?', '-1', commitHash], { cwd: context.repoDir })
219
+
220
+ return {
221
+ commitHash,
222
+ did: `did:repo:${commitHash}`,
223
+ fullDetails: showResult.stdout,
224
+ committer: committerResult.stdout.trim(),
225
+ keyFingerprint: keyResult.stdout.trim(),
226
+ signatureStatus: sigStatusResult.stdout.trim(),
227
+ }
228
+ }
229
+ },
230
+
231
+ verifyInceptionCommit: {
232
+ type: CapsulePropertyTypes.Function,
233
+ value: async function (this: any, context: {
234
+ repoDir: string
235
+ allowedSigners: AllowedSigner[]
236
+ }) {
237
+ const hashResult = await git(['rev-list', '--max-parents=0', 'HEAD'], { cwd: context.repoDir })
238
+ if (hashResult.exitCode !== 0) {
239
+ throw new Error(`Failed to get inception commit: ${hashResult.stderr}`)
240
+ }
241
+ const commitHash = hashResult.stdout.split('\n')[0].trim()
242
+
243
+ return withAllowedSigners(context.allowedSigners, async (cfg) => {
244
+ const verifyResult = await git([...cfg, 'verify-commit', commitHash], { cwd: context.repoDir })
245
+ const output = verifyResult.stderr || verifyResult.stdout
246
+ const isGood = output.includes('Good "git" signature')
247
+
248
+ const verboseResult = await git([...cfg, 'verify-commit', '-v', commitHash], { cwd: context.repoDir })
249
+
250
+ return {
251
+ commitHash,
252
+ valid: isGood,
253
+ exitCode: verifyResult.exitCode,
254
+ output,
255
+ verboseOutput: verboseResult.stderr || verboseResult.stdout,
256
+ }
257
+ })
258
+ }
259
+ },
260
+
261
+ verifyCommit: {
262
+ type: CapsulePropertyTypes.Function,
263
+ value: async function (this: any, context: {
264
+ repoDir: string
265
+ commitHash?: string
266
+ allowedSigners: AllowedSigner[]
267
+ }) {
268
+ const hash = context.commitHash || 'HEAD'
269
+
270
+ return withAllowedSigners(context.allowedSigners, async (cfg) => {
271
+ const verifyResult = await git([...cfg, 'verify-commit', hash], { cwd: context.repoDir })
272
+ const output = verifyResult.stderr || verifyResult.stdout
273
+ const isGood = output.includes('Good "git" signature')
274
+
275
+ const sigStatusResult = await git(['log', '--format=%G?|%GK|%GS', '-1', hash], { cwd: context.repoDir })
276
+ const [status, keyFingerprint, signer] = sigStatusResult.stdout.split('|')
277
+
278
+ return {
279
+ commitHash: hash,
280
+ valid: isGood,
281
+ exitCode: verifyResult.exitCode,
282
+ output,
283
+ signatureStatus: status,
284
+ keyFingerprint,
285
+ signer,
286
+ }
287
+ })
288
+ }
289
+ },
290
+
291
+ listCommits: {
292
+ type: CapsulePropertyTypes.Function,
293
+ value: async function (this: any, context: {
294
+ repoDir: string
295
+ reverse?: boolean
296
+ }) {
297
+ const args = ['log', '--oneline', '--format=%H|%s|%G?|%GK']
298
+ if (context.reverse) {
299
+ args.push('--reverse')
300
+ }
301
+
302
+ const result = await git(args, { cwd: context.repoDir })
303
+ if (result.exitCode !== 0) {
304
+ throw new Error(`Failed to list commits: ${result.stderr}`)
305
+ }
306
+
307
+ const commits = result.stdout.split('\n')
308
+ .filter(l => l.trim() !== '')
309
+ .map(line => {
310
+ const [hash, message, sigStatus, keyFingerprint] = line.split('|')
311
+ return { hash, message, signatureStatus: sigStatus, keyFingerprint }
312
+ })
313
+
314
+ return { commits, count: commits.length }
315
+ }
316
+ },
317
+
318
+ getCommitDetails: {
319
+ type: CapsulePropertyTypes.Function,
320
+ value: async function (this: any, context: {
321
+ repoDir: string
322
+ commitHash?: string
323
+ }) {
324
+ const hash = context.commitHash || 'HEAD'
325
+
326
+ const showResult = await git(['show', '--pretty=fuller', hash], { cwd: context.repoDir })
327
+ const formatResult = await git([
328
+ 'log', '--format=%H|%an|%ae|%cn|%ce|%s|%G?|%GK', '-1', hash
329
+ ], { cwd: context.repoDir })
330
+
331
+ const parts = formatResult.stdout.split('|')
332
+ return {
333
+ commitHash: parts[0],
334
+ authorName: parts[1],
335
+ authorEmail: parts[2],
336
+ committerName: parts[3],
337
+ committerEmail: parts[4],
338
+ message: parts[5],
339
+ signatureStatus: parts[6],
340
+ keyFingerprint: parts[7],
341
+ fullDetails: showResult.stdout,
342
+ }
343
+ }
344
+ },
345
+
346
+ createSignedCommit: {
347
+ type: CapsulePropertyTypes.Function,
348
+ value: async function (this: any, context: {
349
+ repoDir: string
350
+ signingKeyPath: string
351
+ message: string
352
+ authorName: string
353
+ authorEmail: string
354
+ files?: Array<{ path: string; content: string }>
355
+ allowEmpty?: boolean
356
+ }) {
357
+ // Write files and stage them
358
+ if (context.files && context.files.length > 0) {
359
+ for (const file of context.files) {
360
+ const absPath = join(context.repoDir, file.path)
361
+ await mkdir(dirname(absPath), { recursive: true })
362
+ await writeFile(absPath, file.content, 'utf-8')
363
+
364
+ const addResult = await git(['add', file.path], { cwd: context.repoDir })
365
+ if (addResult.exitCode !== 0) {
366
+ throw new Error(`git add failed for ${file.path}: ${addResult.stderr}`)
367
+ }
368
+ }
369
+ }
370
+
371
+ const env: Record<string, string> = {
372
+ GIT_AUTHOR_NAME: context.authorName,
373
+ GIT_AUTHOR_EMAIL: context.authorEmail,
374
+ GIT_COMMITTER_NAME: context.authorName,
375
+ GIT_COMMITTER_EMAIL: context.authorEmail,
376
+ }
377
+
378
+ const args = [
379
+ '-c', 'gpg.format=ssh',
380
+ '-c', `user.signingkey=${context.signingKeyPath}`,
381
+ 'commit',
382
+ '--gpg-sign',
383
+ '--signoff',
384
+ '-m', context.message,
385
+ ]
386
+
387
+ if (context.allowEmpty) {
388
+ args.push('--allow-empty')
389
+ }
390
+
391
+ const commitResult = await git(args, { cwd: context.repoDir, env })
392
+
393
+ if (commitResult.exitCode !== 0) {
394
+ throw new Error(`Signed commit failed: ${commitResult.stderr}`)
395
+ }
396
+
397
+ const hashResult = await git(['rev-parse', 'HEAD'], { cwd: context.repoDir })
398
+
399
+ return {
400
+ commitHash: hashResult.stdout.trim(),
401
+ message: context.message,
402
+ }
403
+ }
404
+ },
405
+
406
+ auditRepository: {
407
+ type: CapsulePropertyTypes.Function,
408
+ value: async function (this: any, context: {
409
+ repoDir: string
410
+ allowedSigners: AllowedSigner[]
411
+ }) {
412
+ return withAllowedSigners(context.allowedSigners, async (cfg) => {
413
+ const results: any[] = []
414
+
415
+ // Get all commits oldest first
416
+ const listResult = await git([
417
+ 'log', '--oneline', '--format=%H|%s|%G?|%GK|%an|%cn', '--reverse'
418
+ ], { cwd: context.repoDir })
419
+
420
+ if (listResult.exitCode !== 0) {
421
+ throw new Error(`Failed to list commits: ${listResult.stderr}`)
422
+ }
423
+
424
+ const commits = listResult.stdout.split('\n').filter(l => l.trim() !== '')
425
+
426
+ // Check inception commit
427
+ const inceptionHash = commits[0]?.split('|')[0]
428
+ const inceptionVerify = await git([...cfg, 'verify-commit', inceptionHash], { cwd: context.repoDir })
429
+ const inceptionOutput = inceptionVerify.stderr || inceptionVerify.stdout
430
+ const inceptionValid = inceptionOutput.includes('Good "git" signature')
431
+
432
+ // Check if inception commit is empty (empty tree)
433
+ const treeResult = await git(['log', '--format=%T', '-1', inceptionHash], { cwd: context.repoDir })
434
+ const isEmptyTree = treeResult.stdout.trim() === '4b825dc642cb6eb9a060e54bf8d69288fbee4904'
435
+
436
+ for (const line of commits) {
437
+ const [hash, message, sigStatus, keyFingerprint, authorName, committerName] = line.split('|')
438
+ const verifyResult = await git([...cfg, 'verify-commit', hash], { cwd: context.repoDir })
439
+ const verifyOutput = verifyResult.stderr || verifyResult.stdout
440
+ const valid = verifyOutput.includes('Good "git" signature')
441
+
442
+ results.push({
443
+ hash,
444
+ message,
445
+ signatureStatus: sigStatus,
446
+ keyFingerprint,
447
+ authorName,
448
+ committerName,
449
+ signatureValid: valid,
450
+ verifyOutput,
451
+ })
452
+ }
453
+
454
+ return {
455
+ totalCommits: results.length,
456
+ validSignatures: results.filter(r => r.signatureValid).length,
457
+ invalidSignatures: results.filter(r => !r.signatureValid).length,
458
+ inceptionCommitValid: inceptionValid,
459
+ inceptionCommitEmpty: isEmptyTree,
460
+ did: `did:repo:${inceptionHash}`,
461
+ commits: results,
462
+ }
463
+ })
464
+ }
465
+ },
466
+
467
+ exec: {
468
+ type: CapsulePropertyTypes.Function,
469
+ value: async function (this: any, context: {
470
+ args: string[]
471
+ cwd: string
472
+ }) {
473
+ return git(context.args, { cwd: context.cwd })
474
+ }
475
+ },
476
+
477
+ }
478
+ }
479
+ }, {
480
+ importMeta: import.meta,
481
+ importStack: makeImportStack(),
482
+ capsuleName: capsule['#'],
483
+ })
484
+ }
485
+ capsule['#'] = 't44/caps/providers/blockchaincommons.com/open-integrity-js'
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env bun test
2
+
3
+ import * as bunTest from 'bun:test'
4
+ import { run } from 't44/workspace-rt'
5
+ import { join } from 'path'
6
+ import { rm, mkdir } from 'fs/promises'
7
+
8
+ const WORK_DIR = join(import.meta.dir, '.~open-integrity-sh')
9
+
10
+ const {
11
+ test: { describe, it, expect },
12
+ oi
13
+ } = await run(async ({ encapsulate, CapsulePropertyTypes, makeImportStack }: any) => {
14
+ const spine = await encapsulate({
15
+ '#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
16
+ '#@stream44.studio/encapsulate/structs/Capsule': {},
17
+ '#': {
18
+ test: {
19
+ type: CapsulePropertyTypes.Mapping,
20
+ value: 't44/caps/WorkspaceTest',
21
+ options: {
22
+ '#': {
23
+ bunTest,
24
+ env: {}
25
+ }
26
+ }
27
+ },
28
+ oi: {
29
+ type: CapsulePropertyTypes.Mapping,
30
+ value: './open-integrity-sh'
31
+ },
32
+ }
33
+ }
34
+ }, {
35
+ importMeta: import.meta,
36
+ importStack: makeImportStack(),
37
+ capsuleName: 't44/caps/providers/blockchaincommons.com/open-integrity-sh.test'
38
+ })
39
+ return { spine }
40
+ }, async ({ spine, apis }: any) => {
41
+ return apis[spine.capsuleSourceLineRef]
42
+ }, {
43
+ importMeta: import.meta
44
+ })
45
+
46
+ // Clean up before tests
47
+ await rm(WORK_DIR, { recursive: true, force: true })
48
+ await mkdir(WORK_DIR, { recursive: true })
49
+
50
+ describe('Open Integrity SH (shell script delegation)', function () {
51
+
52
+ const repoDir = join(WORK_DIR, 'test-repo')
53
+
54
+ describe('1. setup_git_inception_repo.sh', function () {
55
+
56
+ it('should create an inception repo via the shell script', async function () {
57
+ const result = await oi.createInceptionRepo({
58
+ repoDir,
59
+ force: false,
60
+ })
61
+
62
+ expect(result.exitCode).toBe(0)
63
+ })
64
+
65
+ it('should fail when repo already exists without --force', async function () {
66
+ try {
67
+ await oi.createInceptionRepo({
68
+ repoDir,
69
+ force: false,
70
+ })
71
+ expect(true).toBe(false) // should not reach here
72
+ } catch (err: any) {
73
+ expect(err.message).toContain('setup_git_inception_repo.sh failed')
74
+ }
75
+ })
76
+
77
+ it('should succeed with --force on existing repo', async function () {
78
+ const forceRepoDir = join(WORK_DIR, 'test-repo-force')
79
+ await oi.createInceptionRepo({ repoDir: forceRepoDir })
80
+ const result = await oi.createInceptionRepo({ repoDir: forceRepoDir, force: true })
81
+ expect(result.exitCode).toBe(0)
82
+ })
83
+ })
84
+
85
+ describe('2. get_repo_did.sh', function () {
86
+
87
+ it('should retrieve the repo DID', async function () {
88
+ const did = await oi.getRepoDid({ repoDir })
89
+
90
+ expect(did).toStartWith('did:repo:')
91
+ expect(did.length).toBeGreaterThan('did:repo:'.length)
92
+ })
93
+
94
+ it('should fail on a non-repo directory', async function () {
95
+ const badDir = join('/tmp', 'not-a-git-repo-' + Date.now())
96
+ await mkdir(badDir, { recursive: true })
97
+
98
+ try {
99
+ await oi.getRepoDid({ repoDir: badDir })
100
+ expect(true).toBe(false) // should not reach here
101
+ } catch (err: any) {
102
+ expect(err.message).toContain('get_repo_did.sh failed')
103
+ }
104
+ })
105
+ })
106
+
107
+ describe('3. audit_inception_commit-POC.sh', function () {
108
+
109
+ it('should pass audit on a valid inception repo', async function () {
110
+ const result = await oi.auditInceptionCommit({
111
+ repoDir,
112
+ quiet: true,
113
+ noPrompt: true,
114
+ noColor: true,
115
+ })
116
+
117
+ expect(result.passed).toBe(true)
118
+ expect(result.exitCode).toBe(0)
119
+ })
120
+
121
+ it('should return verbose output when requested', async function () {
122
+ const result = await oi.auditInceptionCommit({
123
+ repoDir,
124
+ verbose: true,
125
+ noPrompt: true,
126
+ noColor: true,
127
+ })
128
+
129
+ expect(result.exitCode).toBe(0)
130
+ expect(result.stdout.length).toBeGreaterThan(0)
131
+ })
132
+ })
133
+
134
+ describe('4. snippet_template.sh', function () {
135
+
136
+ it('should show file status in default format', async function () {
137
+ const testFile = join(WORK_DIR, 'test-file.txt')
138
+ const { writeFile } = await import('fs/promises')
139
+ await writeFile(testFile, 'hello world')
140
+
141
+ const result = await oi.showFileStatus({
142
+ filePath: testFile,
143
+ })
144
+
145
+ expect(result.exitCode).toBe(0)
146
+ expect(result.stdout).toContain('File:')
147
+ expect(result.stdout).toContain('Size:')
148
+ })
149
+
150
+ it('should show file status in json format', async function () {
151
+ const testFile = join(WORK_DIR, 'test-file.txt')
152
+
153
+ const result = await oi.showFileStatus({
154
+ filePath: testFile,
155
+ format: 'json',
156
+ })
157
+
158
+ expect(result.exitCode).toBe(0)
159
+ expect(result.stdout).toContain('"file"')
160
+ expect(result.stdout).toContain('"size"')
161
+ })
162
+ })
163
+
164
+ describe('5. Cross-script workflow', function () {
165
+
166
+ it('should create repo, get DID, and audit in sequence', async function () {
167
+ const workflowRepoDir = join(WORK_DIR, 'workflow-repo')
168
+
169
+ // Create inception repo
170
+ const createResult = await oi.createInceptionRepo({ repoDir: workflowRepoDir })
171
+ expect(createResult.exitCode).toBe(0)
172
+
173
+ // Get DID
174
+ const did = await oi.getRepoDid({ repoDir: workflowRepoDir })
175
+ expect(did).toStartWith('did:repo:')
176
+
177
+ // Audit
178
+ const auditResult = await oi.auditInceptionCommit({
179
+ repoDir: workflowRepoDir,
180
+ quiet: true,
181
+ noPrompt: true,
182
+ noColor: true,
183
+ })
184
+ expect(auditResult.passed).toBe(true)
185
+ })
186
+ })
187
+
188
+ })