@zhushanwen/pi-session-reader 0.6.1 → 0.8.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 +100 -0
- package/package.json +6 -3
- package/src/__tests__/entry-anchor.test.ts +370 -0
- package/src/__tests__/execution-tree.test.ts +12 -12
- package/src/__tests__/family-zcode.test.ts +255 -0
- package/src/__tests__/family.test.ts +1 -1
- package/src/__tests__/find.test.ts +32 -49
- package/src/__tests__/fixtures.ts +15 -0
- package/src/__tests__/parser.test.ts +37 -13
- package/src/__tests__/render.test.ts +22 -19
- package/src/__tests__/result.test.ts +2 -2
- package/src/__tests__/roots.test.ts +32 -34
- package/src/__tests__/subagents.test.ts +131 -108
- package/src/__tests__/tool-handler.test.ts +506 -153
- package/src/__tests__/toolcall.test.ts +1 -1
- package/src/__tests__/tree.test.ts +1 -1
- package/src/__tests__/turns.test.ts +21 -20
- package/src/__tests__/workflow.test.ts +319 -23
- package/src/__tests__/zcode-manifest.test.ts +361 -0
- package/src/__tests__/zcode-routing.test.ts +751 -0
- package/src/__tests__/zcode-whitelist.test.ts +119 -0
- package/src/core/execution-tree.ts +3 -1
- package/src/core/family.ts +80 -1
- package/src/core/render.ts +1 -1
- package/src/core/toolcall.ts +2 -2
- package/src/core/tree.ts +1 -1
- package/src/core/turns.ts +1 -1
- package/src/core/workflow.ts +196 -6
- package/src/discovery/entry-anchor.ts +139 -0
- package/src/discovery/roots.ts +1 -2
- package/src/discovery/session-header.ts +30 -69
- package/src/discovery/subagents.ts +9 -2
- package/src/discovery/whitelist.ts +87 -0
- package/src/discovery/workflows.ts +232 -22
- package/src/discovery/zcode-manifest.ts +277 -0
- package/src/index.ts +7 -0
- package/src/result-action.ts +14 -5
- package/src/search-across.ts +1 -1
- package/src/tool-format.ts +455 -0
- package/src/tool-handler.ts +400 -344
- package/src/zcode-anchor-classify.ts +74 -0
- package/src/__tests__/real-data.ts +0 -96
- package/src/core/parser.ts +0 -167
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
2
|
import { extractToolCalls, formatToolCallSummary } from '../core/toolcall.js'
|
|
3
|
-
import type { Entry } from '
|
|
3
|
+
import type { Entry } from '@zhushanwen/session-core'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* D1 映射表(design §3.3)确定性测试:formatToolCallSummary 12 个 switch 分支
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
2
|
import { buildTreeView } from '../core/tree.js'
|
|
3
|
-
import type { Entry } from '
|
|
3
|
+
import type { Entry } from '@zhushanwen/session-core'
|
|
4
4
|
|
|
5
5
|
/** 构造最小 Entry(tree.ts 只消费 type/id/parentId,其余字段省略) */
|
|
6
6
|
function e(id: string, parentId: string | null): Entry {
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
2
|
import { segmentTurns } from '../core/turns.js'
|
|
3
|
-
import type { Entry } from '
|
|
4
|
-
import { parseSessionFile } from '../core/parser.js'
|
|
3
|
+
import type { Entry } from '@zhushanwen/session-core'
|
|
5
4
|
import { buildTreeView } from '../core/tree.js'
|
|
6
|
-
import { REAL_SESSION, HAS_REAL_SESSION } from './real-data.js'
|
|
7
5
|
|
|
8
6
|
// ---- Entry 构造助手(turns.ts 只消费 type/id/parentId/message.role) ----
|
|
9
7
|
|
|
@@ -121,23 +119,26 @@ describe('segmentTurns', () => {
|
|
|
121
119
|
expect(turns[0].startTime).toBe('2026-05-28T03:17:12.844Z')
|
|
122
120
|
})
|
|
123
121
|
|
|
124
|
-
it
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
122
|
+
it('leaf 视图分段结构形态:preface(首 user 前的 model_change)+ user turns + compaction turn', () => {
|
|
123
|
+
// 合成分段结构形态(1 preface + 1 compaction + 2 user turn):
|
|
124
|
+
// 规则 2(compaction 独立成 turn)+ 规则 4(首 user 前的 model_change/thinking_level_change
|
|
125
|
+
// 成 preface turn,无 userEntry)
|
|
126
|
+
const entries = [
|
|
127
|
+
entry('MC', null, 'model_change'),
|
|
128
|
+
msg('U1', 'MC', 'user'),
|
|
129
|
+
msg('A1', 'U1', 'assistant'),
|
|
130
|
+
entry('C1', 'A1', 'compaction'),
|
|
131
|
+
msg('U2', 'C1', 'user'),
|
|
132
|
+
msg('A2', 'U2', 'assistant'),
|
|
133
|
+
]
|
|
134
|
+
const tree = buildTreeView(entries)
|
|
135
|
+
const turns = segmentTurns(entries, new Set(tree.leafPath))
|
|
136
|
+
|
|
137
|
+
expect(turns.length).toBe(4)
|
|
138
|
+
// 首 turn 是 preface(model_change),无 userEntry、非 compaction
|
|
135
139
|
expect(turns[0].userEntry).toBeUndefined()
|
|
136
140
|
expect(turns[0].isCompaction).toBe(false)
|
|
137
|
-
|
|
138
|
-
expect(turns.filter((t) => t.
|
|
139
|
-
|
|
140
|
-
expect(turns.filter((t) => t.userEntry !== undefined).length).toBe(26)
|
|
141
|
-
// 5.6MB 全量解析在并发/高负载下可能超 vitest 默认 5s,显式放宽
|
|
142
|
-
}, 60000)
|
|
141
|
+
expect(turns.filter((t) => t.isCompaction).length).toBe(1)
|
|
142
|
+
expect(turns.filter((t) => t.userEntry !== undefined).length).toBe(2)
|
|
143
|
+
})
|
|
143
144
|
})
|
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
import { describe, it, expect, afterEach } from 'vitest'
|
|
2
2
|
import { tmpdir } from 'node:os'
|
|
3
|
-
import {
|
|
4
|
-
import { mkdtemp, writeFile, rm } from 'node:fs/promises'
|
|
3
|
+
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'
|
|
5
4
|
import { join } from 'node:path'
|
|
6
5
|
|
|
6
|
+
import {
|
|
7
|
+
WORKFLOW_RECORD_CUSTOM_TYPE,
|
|
8
|
+
WORKFLOW_RECORD_ENTRY_VERSION,
|
|
9
|
+
WORKFLOW_STATE_LINK_CUSTOM_TYPE,
|
|
10
|
+
} from '@zhushanwen/subagent-core'
|
|
11
|
+
|
|
7
12
|
import { parseRunSnapshot, renderWorkflowOverview } from '../core/workflow.js'
|
|
8
|
-
import {
|
|
9
|
-
|
|
13
|
+
import {
|
|
14
|
+
extractCallSessionFiles,
|
|
15
|
+
extractRecordStreamSessionFiles,
|
|
16
|
+
readRunSnapshot,
|
|
17
|
+
resolveWorkflows,
|
|
18
|
+
RUN_RECORD_STREAM_SUFFIX,
|
|
19
|
+
} from '../discovery/workflows.js'
|
|
10
20
|
|
|
11
21
|
// ============================================================
|
|
12
22
|
// fixture(结构对齐真实 wf-state 探针数据)
|
|
13
23
|
// ============================================================
|
|
14
24
|
|
|
15
25
|
/**
|
|
16
|
-
* NEW 格式 fixture
|
|
26
|
+
* NEW 格式 fixture(快照字段形态 = pi-subagent-workflow 写侧落盘契约)。
|
|
17
27
|
* runId 故意写成 'wf-ignore' 验证 parseRunSnapshot 用参数透传不读 snapshot.runId。
|
|
18
28
|
*/
|
|
19
29
|
const NEW_SNAPSHOT_FIXTURE = {
|
|
@@ -367,39 +377,325 @@ describe('readRunSnapshot', () => {
|
|
|
367
377
|
})
|
|
368
378
|
|
|
369
379
|
// ============================================================
|
|
370
|
-
//
|
|
380
|
+
// resolveWorkflows 三档发现链(W1 D10 断链修复:v2 journalPath 主源 / v1 快照层 /
|
|
381
|
+
// 旧指针 fallback——W17 前 / W17~W1 / W1+ 三类 run 全部有发现通道)
|
|
382
|
+
// ============================================================
|
|
383
|
+
|
|
384
|
+
/** v1 快照对象(RunSnapshot 形态:v/runId/state.calls——v1 workflow-record 条目的 snapshot 载荷)。 */
|
|
385
|
+
function v1Snapshot(runId: string, sessionFiles: string[]): Record<string, unknown> {
|
|
386
|
+
return {
|
|
387
|
+
v: 'wf-run-v2',
|
|
388
|
+
runId,
|
|
389
|
+
spec: { scriptName: 'mid-window', name: 'Mid' },
|
|
390
|
+
state: {
|
|
391
|
+
status: 'done',
|
|
392
|
+
reason: 'completed',
|
|
393
|
+
budget: { usedTokens: 10, usedCost: 0, totalCallCount: sessionFiles.length, maxTokens: 1000 },
|
|
394
|
+
calls: sessionFiles.map((sf, i) => ({
|
|
395
|
+
id: i,
|
|
396
|
+
opts: { prompt: 'work', model: 'm', description: `step-${i}` },
|
|
397
|
+
status: 'done',
|
|
398
|
+
attempts: 1,
|
|
399
|
+
result: { content: 'ok', durationMs: 1, sessionId: `019v1${i}`, sessionFile: sf },
|
|
400
|
+
})),
|
|
401
|
+
},
|
|
402
|
+
meta: { startedAt: '2026-09-20T00:00:00.000Z', completedAt: '2026-09-20T00:01:00.000Z' },
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
describe('resolveWorkflows 三档发现链(W1 D10)', () => {
|
|
407
|
+
let dir: string
|
|
408
|
+
afterEach(async () => {
|
|
409
|
+
if (dir) await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 }).catch(() => {})
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
/** 写 main session 文件(首行 header + 追加行集);返回路径与 sessionIdToPath 单条目映射。 */
|
|
413
|
+
async function writeMain(lines: string[]): Promise<{
|
|
414
|
+
path: string
|
|
415
|
+
sessionIdToPath: Map<string, string>
|
|
416
|
+
}> {
|
|
417
|
+
const sessionId = 'main-session-1'
|
|
418
|
+
const path = join(dir, `${sessionId}.jsonl`)
|
|
419
|
+
await writeFile(path, [JSON.stringify({ type: 'session', id: sessionId, cwd: '/proj' }), ...lines].join('\n') + '\n')
|
|
420
|
+
return { path, sessionIdToPath: new Map([[sessionId, path]]) }
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** 写 wf-state 快照文件(尾行最新快照)。返回绝对路径。 */
|
|
424
|
+
async function writeStateFile(runId: string, snapshot: Record<string, unknown>): Promise<string> {
|
|
425
|
+
const wfDir = join(dir, 'workflow-state')
|
|
426
|
+
await mkdir(wfDir, { recursive: true })
|
|
427
|
+
const path = join(wfDir, `${runId}.jsonl`)
|
|
428
|
+
await writeFile(path, JSON.stringify(snapshot) + '\n')
|
|
429
|
+
return path
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** 写 record 流文件([D16③] v2 档数据源——目录自建)。 */
|
|
433
|
+
async function writeRecordStream(runId: string, lines: string[]): Promise<string> {
|
|
434
|
+
const wfDir = join(dir, 'workflow-state')
|
|
435
|
+
await mkdir(wfDir, { recursive: true })
|
|
436
|
+
const p = join(wfDir, `${runId}.record.jsonl`)
|
|
437
|
+
await writeFile(p, lines.join('\n') + '\n')
|
|
438
|
+
return p
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** workflow-record v2 注册条目行(journalPath 锚点)。 */
|
|
442
|
+
function v2RegisteredLine(runId: string, journalPath: string): string {
|
|
443
|
+
return JSON.stringify({
|
|
444
|
+
type: 'custom',
|
|
445
|
+
customType: 'workflow-record',
|
|
446
|
+
id: `e-${runId}`,
|
|
447
|
+
parentId: null,
|
|
448
|
+
data: {
|
|
449
|
+
v: 2,
|
|
450
|
+
kind: 'registered',
|
|
451
|
+
runId,
|
|
452
|
+
workflowName: 'probe-flow',
|
|
453
|
+
scriptName: 'probe-flow',
|
|
454
|
+
slug: 'probe',
|
|
455
|
+
startedAt: 1,
|
|
456
|
+
journalPath,
|
|
457
|
+
},
|
|
458
|
+
})
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** workflow-record v1 快照条目行(W17~W1 中间档形态:{v:1, snapshot, updatedAt})。 */
|
|
462
|
+
function v1SnapshotLine(runId: string, sessionFiles: string[]): string {
|
|
463
|
+
return JSON.stringify({
|
|
464
|
+
type: 'custom',
|
|
465
|
+
customType: 'workflow-record',
|
|
466
|
+
id: `e-${runId}`,
|
|
467
|
+
parentId: null,
|
|
468
|
+
data: { v: 1, snapshot: v1Snapshot(runId, sessionFiles), updatedAt: 2 },
|
|
469
|
+
})
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** workflow-record v2 终态条目行(不携带 journalPath——非发现链数据源)。 */
|
|
473
|
+
function v2SettledLine(runId: string): string {
|
|
474
|
+
return JSON.stringify({
|
|
475
|
+
type: 'custom',
|
|
476
|
+
customType: 'workflow-record',
|
|
477
|
+
id: `e-settled-${runId}`,
|
|
478
|
+
parentId: null,
|
|
479
|
+
data: {
|
|
480
|
+
v: 2,
|
|
481
|
+
kind: 'settled',
|
|
482
|
+
runId,
|
|
483
|
+
status: 'done',
|
|
484
|
+
reason: 'completed',
|
|
485
|
+
outcome: 'success',
|
|
486
|
+
settledAt: 3,
|
|
487
|
+
callCount: 1,
|
|
488
|
+
usedTokens: 10,
|
|
489
|
+
},
|
|
490
|
+
})
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** 旧指针条目行(W17 前形态:workflow-state-link)。 */
|
|
494
|
+
function linkLine(runId: string, path: string): string {
|
|
495
|
+
return JSON.stringify({
|
|
496
|
+
type: 'custom',
|
|
497
|
+
customType: 'workflow-state-link',
|
|
498
|
+
id: `e-link-${runId}`,
|
|
499
|
+
parentId: null,
|
|
500
|
+
data: { runId, path, updatedAt: '2026-08-07T16:48:24.933Z' },
|
|
501
|
+
})
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const CALL_A = '/abs/subagents/--proj--/sessions/call-a.jsonl'
|
|
505
|
+
const CALL_B = '/abs/subagents/--proj--/sessions/call-b.jsonl'
|
|
506
|
+
|
|
507
|
+
it('v2 journalPath 主源([D16③] 重锚):注册条目 → record 流直读提 calls,stateFile = 流路径', async () => {
|
|
508
|
+
dir = await mkdtemp(join(tmpdir(), 'wf-v2-tier-'))
|
|
509
|
+
// record 流内容:run-created + agent-started + agent-settled(result 携带 sessionFile)
|
|
510
|
+
const recordPath = await writeRecordStream('wf-v2-1', [
|
|
511
|
+
JSON.stringify({ type: 'run-created', seq: 1, ts: 1000, runId: 'wf-v2-1', workflowName: 'v2-flow', argsSummary: '{}' }),
|
|
512
|
+
JSON.stringify({ type: 'agent-started', seq: 2, ts: 1100, taskIndex: 0, agentName: 'step-0', attempt: 1 }),
|
|
513
|
+
JSON.stringify({ type: 'agent-settled', seq: 3, ts: 1500, taskIndex: 0, attempt: 1, outcome: 'done', durationMs: 400, result: { content: 'ok', sessionFile: CALL_A } }),
|
|
514
|
+
])
|
|
515
|
+
const { sessionIdToPath } = await writeMain([v2RegisteredLine('wf-v2-1', recordPath)])
|
|
516
|
+
|
|
517
|
+
const workflows = await resolveWorkflows('main-session-1', sessionIdToPath, new Map())
|
|
518
|
+
|
|
519
|
+
expect(workflows).toHaveLength(1)
|
|
520
|
+
expect(workflows[0].runId).toBe('wf-v2-1')
|
|
521
|
+
// stateFile = record 流路径(journalPath 锚点语义重定义——[D16③])
|
|
522
|
+
expect(workflows[0].stateFile).toBe(recordPath)
|
|
523
|
+
expect(workflows[0].calls).toHaveLength(1)
|
|
524
|
+
expect(workflows[0].calls[0].fileName).toBe(CALL_A)
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
it('v2 档窗外:record 流已被保留期清理 → calls=[],runId 存在性兜底(stateFile 仍是流路径)', async () => {
|
|
528
|
+
dir = await mkdtemp(join(tmpdir(), 'wf-v2-gc-'))
|
|
529
|
+
const recordPath = join(dir, 'workflow-state', 'wf-gc-1.record.jsonl')
|
|
530
|
+
const { sessionIdToPath } = await writeMain([v2RegisteredLine('wf-gc-1', recordPath)])
|
|
531
|
+
|
|
532
|
+
const workflows = await resolveWorkflows('main-session-1', sessionIdToPath, new Map())
|
|
533
|
+
|
|
534
|
+
expect(workflows).toHaveLength(1)
|
|
535
|
+
expect(workflows[0].runId).toBe('wf-gc-1')
|
|
536
|
+
expect(workflows[0].calls).toEqual([])
|
|
537
|
+
// 窗外语义 = 锚点路径在、流文件不在(readFile undefined → calls 空)
|
|
538
|
+
expect(workflows[0].stateFile).toBe(recordPath)
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
it('v2 档旧后缀锚点(.events.jsonl 形态 = [D1] 历史实体)与形态损坏 → stateFile 原样保留 + calls 空(不读旧两件、不伪造数据)', async () => {
|
|
542
|
+
dir = await mkdtemp(join(tmpdir(), 'wf-v2-bad-'))
|
|
543
|
+
// 旧后缀锚点:[D1] 历史数据处置——旧两件不读(calls 退空,run 存在性兜底)
|
|
544
|
+
const legacyPath = join(dir, 'workflow-state', 'wf-legacy-1.events.jsonl')
|
|
545
|
+
await mkdir(join(dir, 'workflow-state'), { recursive: true })
|
|
546
|
+
await writeFile(legacyPath, JSON.stringify({ type: 'ask-settled', ts: 1 }) + '\n')
|
|
547
|
+
const legacyMain = await writeMain([v2RegisteredLine('wf-legacy-1', legacyPath)])
|
|
548
|
+
const legacy = await resolveWorkflows('main-session-1', legacyMain.sessionIdToPath, new Map())
|
|
549
|
+
expect(legacy).toHaveLength(1)
|
|
550
|
+
expect(legacy[0].runId).toBe('wf-legacy-1')
|
|
551
|
+
expect(legacy[0].stateFile).toBe(legacyPath)
|
|
552
|
+
expect(legacy[0].calls).toEqual([])
|
|
553
|
+
|
|
554
|
+
// 形态损坏(无已知尾段):同旧锚点分流
|
|
555
|
+
const oddMain = await writeMain([v2RegisteredLine('wf-bad-1', '/abs/odd/path.txt')])
|
|
556
|
+
const odd = await resolveWorkflows('main-session-1', oddMain.sessionIdToPath, new Map())
|
|
557
|
+
expect(odd).toHaveLength(1)
|
|
558
|
+
expect(odd[0].runId).toBe('wf-bad-1')
|
|
559
|
+
expect(odd[0].stateFile).toBe('/abs/odd/path.txt')
|
|
560
|
+
expect(odd[0].calls).toEqual([])
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
it('v1 快照层(W17~W1 中间档恢复):v1 快照条目直接提 snapshot.calls,无指针条目也可见', async () => {
|
|
564
|
+
// 该档现状发现链恒空(只认指针)——本用例锚定中间档恢复:升级窗口期的旧格式会话
|
|
565
|
+
dir = await mkdtemp(join(tmpdir(), 'wf-v1-tier-'))
|
|
566
|
+
const { sessionIdToPath } = await writeMain([v1SnapshotLine('wf-mid-1', [CALL_A, CALL_B])])
|
|
567
|
+
|
|
568
|
+
const workflows = await resolveWorkflows('main-session-1', sessionIdToPath, new Map())
|
|
569
|
+
|
|
570
|
+
expect(workflows).toHaveLength(1)
|
|
571
|
+
expect(workflows[0].runId).toBe('wf-mid-1')
|
|
572
|
+
// v1 条目不携带 state 路径 → stateFile 空串(概览消费面自然降级,家族链 calls 完整)
|
|
573
|
+
expect(workflows[0].stateFile).toBe('')
|
|
574
|
+
expect(workflows[0].calls.map((c) => c.fileName)).toEqual([CALL_A, CALL_B])
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
it('旧指针 fallback:workflow-state-link 指针条目照旧读 link 指向的快照文件', async () => {
|
|
578
|
+
dir = await mkdtemp(join(tmpdir(), 'wf-link-tier-'))
|
|
579
|
+
const stateFile = await writeStateFile('wf-old-1', v1Snapshot('wf-old-1', [CALL_B]))
|
|
580
|
+
const { sessionIdToPath } = await writeMain([linkLine('wf-old-1', stateFile)])
|
|
581
|
+
|
|
582
|
+
const workflows = await resolveWorkflows('main-session-1', sessionIdToPath, new Map())
|
|
583
|
+
|
|
584
|
+
expect(workflows).toHaveLength(1)
|
|
585
|
+
expect(workflows[0].runId).toBe('wf-old-1')
|
|
586
|
+
expect(workflows[0].stateFile).toBe(stateFile)
|
|
587
|
+
expect(workflows[0].calls.map((c) => c.fileName)).toEqual([CALL_B])
|
|
588
|
+
})
|
|
589
|
+
|
|
590
|
+
it('三档混合会话:三个 run 各处一档 → 各自正确,按条目首见序输出([D16③] v2 档 = record 流直读)', async () => {
|
|
591
|
+
dir = await mkdtemp(join(tmpdir(), 'wf-mixed-'))
|
|
592
|
+
const recordPath = await writeRecordStream('wf-v2-1', [
|
|
593
|
+
JSON.stringify({ type: 'run-created', seq: 1, ts: 1000, runId: 'wf-v2-1', workflowName: 'v2-flow', argsSummary: '{}' }),
|
|
594
|
+
JSON.stringify({ type: 'agent-started', seq: 2, ts: 1100, taskIndex: 0, agentName: 's0', attempt: 1 }),
|
|
595
|
+
JSON.stringify({ type: 'agent-settled', seq: 3, ts: 1500, taskIndex: 0, attempt: 1, outcome: 'done', durationMs: 400, result: { content: 'ok', sessionFile: CALL_A } }),
|
|
596
|
+
])
|
|
597
|
+
const oldState = await writeStateFile('wf-old-1', v1Snapshot('wf-old-1', [CALL_B]))
|
|
598
|
+
// 首见序:指针(早)→ v1 快照(中)→ v2 注册(晚)
|
|
599
|
+
const { sessionIdToPath } = await writeMain([
|
|
600
|
+
linkLine('wf-old-1', oldState),
|
|
601
|
+
v1SnapshotLine('wf-mid-1', [CALL_A]),
|
|
602
|
+
v2RegisteredLine('wf-v2-1', recordPath),
|
|
603
|
+
])
|
|
604
|
+
|
|
605
|
+
const workflows = await resolveWorkflows('main-session-1', sessionIdToPath, new Map())
|
|
606
|
+
|
|
607
|
+
expect(workflows.map((w) => w.runId)).toEqual(['wf-old-1', 'wf-mid-1', 'wf-v2-1'])
|
|
608
|
+
expect(workflows[0].stateFile).toBe(oldState)
|
|
609
|
+
expect(workflows[1].stateFile).toBe('')
|
|
610
|
+
expect(workflows[2].stateFile).toBe(recordPath)
|
|
611
|
+
expect(workflows[2].calls[0].fileName).toBe(CALL_A)
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
it('v2 终态条目(settled,无 journalPath)不参与发现链——不产 runId 幻影', async () => {
|
|
615
|
+
dir = await mkdtemp(join(tmpdir(), 'wf-settled-'))
|
|
616
|
+
const { sessionIdToPath } = await writeMain([v2SettledLine('wf-settled-1')])
|
|
617
|
+
|
|
618
|
+
const workflows = await resolveWorkflows('main-session-1', sessionIdToPath, new Map())
|
|
619
|
+
|
|
620
|
+
expect(workflows).toEqual([])
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
it('同 runId 跨档坏数据防御:v2 注册条目在 → 用 v2 档(高档在即用高档;[D16③] v2 档 = record 流)', async () => {
|
|
624
|
+
dir = await mkdtemp(join(tmpdir(), 'wf-tier-prio-'))
|
|
625
|
+
const recordPath = await writeRecordStream('wf-both-1', [
|
|
626
|
+
JSON.stringify({ type: 'run-created', seq: 1, ts: 1000, runId: 'wf-both-1', workflowName: 'both', argsSummary: '{}' }),
|
|
627
|
+
JSON.stringify({ type: 'agent-started', seq: 2, ts: 1100, taskIndex: 0, agentName: 's0', attempt: 1 }),
|
|
628
|
+
JSON.stringify({ type: 'agent-settled', seq: 3, ts: 1500, taskIndex: 0, attempt: 1, outcome: 'done', durationMs: 400, result: { content: 'ok', sessionFile: CALL_A } }),
|
|
629
|
+
])
|
|
630
|
+
const oldState = await writeStateFile('wf-both-1-old', v1Snapshot('wf-both-1', [CALL_B]))
|
|
631
|
+
const { sessionIdToPath } = await writeMain([
|
|
632
|
+
linkLine('wf-both-1', oldState),
|
|
633
|
+
v2RegisteredLine('wf-both-1', recordPath),
|
|
634
|
+
])
|
|
635
|
+
|
|
636
|
+
const workflows = await resolveWorkflows('main-session-1', sessionIdToPath, new Map())
|
|
637
|
+
|
|
638
|
+
expect(workflows).toHaveLength(1)
|
|
639
|
+
// v2 档胜出:stateFile 是 record 流路径(非 link 的 oldState),calls 来自流
|
|
640
|
+
expect(workflows[0].stateFile).toBe(recordPath)
|
|
641
|
+
expect(workflows[0].calls.map((c) => c.fileName)).toEqual([CALL_A])
|
|
642
|
+
})
|
|
643
|
+
|
|
644
|
+
it('跨包契约守卫:三档判别的本地字面量 === subagent-core 写侧单源([D1] record 后缀)', () => {
|
|
645
|
+
// 源码本地持有磁盘协议字符串与版本/尾段(生产依赖面不引 subagent-core),锚定两侧同值
|
|
646
|
+
expect(WORKFLOW_RECORD_CUSTOM_TYPE).toBe('workflow-record')
|
|
647
|
+
expect(WORKFLOW_RECORD_ENTRY_VERSION).toBe(2)
|
|
648
|
+
expect(RUN_RECORD_STREAM_SUFFIX).toBe('.record.jsonl')
|
|
649
|
+
// legacy 指针档:collectWorkflowEntryTiers 本地字面量与 core 常量同值
|
|
650
|
+
expect(WORKFLOW_STATE_LINK_CUSTOM_TYPE).toBe('workflow-state-link')
|
|
651
|
+
})
|
|
652
|
+
})
|
|
653
|
+
|
|
654
|
+
// ============================================================
|
|
655
|
+
// readRunSnapshot + parseRunSnapshot 磁盘回路(fixture):快照形态已由
|
|
656
|
+
// NEW_SNAPSHOT_FIXTURE / OLD_SNAPSHOT_FIXTURE 单源锚定,此处补「wf-state 落盘 →
|
|
657
|
+
// 读回 → 解析」的端到端回路断言
|
|
371
658
|
// ============================================================
|
|
372
659
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
const HAS_REAL_WF_NEW = existsSync(REAL_WF_NEW)
|
|
376
|
-
const HAS_REAL_WF_OLD = existsSync(REAL_WF_OLD)
|
|
660
|
+
describe('readRunSnapshot + parseRunSnapshot 磁盘回路(fixture)', () => {
|
|
661
|
+
let tmpDir: string | undefined
|
|
377
662
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
663
|
+
afterEach(async () => {
|
|
664
|
+
if (tmpDir !== undefined) {
|
|
665
|
+
await rm(tmpDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
666
|
+
}
|
|
667
|
+
})
|
|
668
|
+
|
|
669
|
+
it('TC-w5-disk-new-guard:NEW 快照落盘读回,类型化为 wf-run-v1 overview', async () => {
|
|
670
|
+
tmpDir = await mkdtemp(join(tmpdir(), 'wf-disk-new-'))
|
|
671
|
+
const wfPath = join(tmpDir, 'wf-new.jsonl')
|
|
672
|
+
await writeFile(wfPath, JSON.stringify(NEW_SNAPSHOT_FIXTURE) + '\n')
|
|
673
|
+
|
|
674
|
+
const snap = await readRunSnapshot(wfPath)
|
|
381
675
|
expect(snap).not.toBeUndefined()
|
|
382
|
-
const overview = parseRunSnapshot(snap, 'wf-
|
|
676
|
+
const overview = parseRunSnapshot(snap, 'wf-disk-new', wfPath)
|
|
383
677
|
expect(overview).not.toBeNull()
|
|
384
678
|
expect(overview!.version).toBe('wf-run-v1')
|
|
385
679
|
expect(overview!.steps.length).toBeGreaterThanOrEqual(1)
|
|
386
|
-
// call 的 sessionFile
|
|
680
|
+
// call 的 sessionFile 是绝对 .jsonl 路径(跳转入口)
|
|
387
681
|
expect(overview!.steps[0].sessionFile).toMatch(/\.jsonl$/)
|
|
388
682
|
expect(overview!.steps[0].sessionFile!.startsWith('/')).toBe(true)
|
|
389
683
|
expect(overview!.steps[0].sessionId).toBeTruthy()
|
|
390
|
-
}
|
|
391
|
-
})
|
|
684
|
+
})
|
|
392
685
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
const
|
|
686
|
+
it('TC-w5-disk-old-guard:OLD 快照落盘读回,尽力解析为 legacy overview', async () => {
|
|
687
|
+
tmpDir = await mkdtemp(join(tmpdir(), 'wf-disk-old-'))
|
|
688
|
+
const wfPath = join(tmpDir, 'wf-old.jsonl')
|
|
689
|
+
await writeFile(wfPath, JSON.stringify(OLD_SNAPSHOT_FIXTURE) + '\n')
|
|
690
|
+
|
|
691
|
+
const snap = await readRunSnapshot(wfPath)
|
|
396
692
|
expect(snap).not.toBeUndefined()
|
|
397
|
-
const overview = parseRunSnapshot(snap, 'wf-skip-ok',
|
|
693
|
+
const overview = parseRunSnapshot(snap, 'wf-skip-ok', wfPath)
|
|
398
694
|
expect(overview).not.toBeNull()
|
|
399
695
|
expect(overview!.version).toBe('legacy')
|
|
400
696
|
expect(overview!.status).toBe('running')
|
|
401
697
|
expect(overview!.script).toBe('workflow-wf-skip-ok') // name 映射
|
|
402
|
-
// OLD callCache value 无 sessionFile
|
|
698
|
+
// OLD callCache value 无 sessionFile → step 无跳转入口
|
|
403
699
|
expect(overview!.steps[0].sessionFile).toBeUndefined()
|
|
404
|
-
}
|
|
700
|
+
})
|
|
405
701
|
})
|