@things-factory/headless-twin 10.0.10 → 10.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-server/engine/canonical-ingest.d.ts +25 -2
- package/dist-server/engine/canonical-ingest.js +48 -10
- package/dist-server/engine/canonical-ingest.js.map +1 -1
- package/dist-server/engine/declared-stimulus.d.ts +43 -0
- package/dist-server/engine/declared-stimulus.js +57 -0
- package/dist-server/engine/declared-stimulus.js.map +1 -0
- package/dist-server/engine/index.d.ts +2 -0
- package/dist-server/engine/index.js +2 -0
- package/dist-server/engine/index.js.map +1 -1
- package/dist-server/engine/kpi-fold.d.ts +12 -0
- package/dist-server/engine/kpi-fold.js +21 -1
- package/dist-server/engine/kpi-fold.js.map +1 -1
- package/dist-server/engine/kpi-query.d.ts +3 -3
- package/dist-server/engine/kpi-query.js +4 -4
- package/dist-server/engine/kpi-query.js.map +1 -1
- package/dist-server/engine/live-feed-registry.d.ts +1 -1
- package/dist-server/engine/live-feed-registry.js +1 -1
- package/dist-server/engine/live-feed-registry.js.map +1 -1
- package/dist-server/engine/local-declarations.d.ts +3 -6
- package/dist-server/engine/local-declarations.js +97 -16
- package/dist-server/engine/local-declarations.js.map +1 -1
- package/dist-server/engine/measured-yield.d.ts +42 -0
- package/dist-server/engine/measured-yield.js +75 -0
- package/dist-server/engine/measured-yield.js.map +1 -0
- package/dist-server/engine/operation-basis.d.ts +16 -0
- package/dist-server/engine/operation-basis.js +20 -2
- package/dist-server/engine/operation-basis.js.map +1 -1
- package/dist-server/engine/property-effects.js +17 -0
- package/dist-server/engine/property-effects.js.map +1 -1
- package/dist-server/engine/restart-policy.d.ts +13 -0
- package/dist-server/engine/restart-policy.js +52 -0
- package/dist-server/engine/restart-policy.js.map +1 -0
- package/dist-server/engine/spec-coverage.d.ts +8 -0
- package/dist-server/engine/spec-coverage.js +3 -1
- package/dist-server/engine/spec-coverage.js.map +1 -1
- package/dist-server/engine/twin-engine.d.ts +48 -15
- package/dist-server/engine/twin-engine.js +179 -39
- package/dist-server/engine/twin-engine.js.map +1 -1
- package/dist-server/index.js +1 -1
- package/dist-server/index.js.map +1 -1
- package/dist-server/service/index.d.ts +1 -1
- package/dist-server/service/reference/control-routing.d.ts +14 -0
- package/dist-server/service/reference/control-routing.js +64 -0
- package/dist-server/service/reference/control-routing.js.map +1 -0
- package/dist-server/service/reference/index.d.ts +1 -0
- package/dist-server/service/reference/index.js +2 -0
- package/dist-server/service/reference/index.js.map +1 -1
- package/dist-server/service/reference/reference-adapter.d.ts +67 -0
- package/dist-server/service/reference/reference-adapter.js +18 -0
- package/dist-server/service/reference/reference-adapter.js.map +1 -1
- package/dist-server/service/reference/reference-live.js +3 -3
- package/dist-server/service/reference/reference-live.js.map +1 -1
- package/dist-server/service/reference/reference-resolver.d.ts +3 -3
- package/dist-server/service/reference/reference-resolver.js +35 -18
- package/dist-server/service/reference/reference-resolver.js.map +1 -1
- package/dist-server/service/twin-audit/command-audit.d.ts +34 -0
- package/dist-server/service/twin-audit/command-audit.js +15 -1
- package/dist-server/service/twin-audit/command-audit.js.map +1 -1
- package/dist-server/service/twin-audit/twin-audit-event.d.ts +3 -0
- package/dist-server/service/twin-audit/twin-audit-event.js +28 -2
- package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -1
- package/dist-server/service/twin-control/twin-control-mutation.d.ts +15 -2
- package/dist-server/service/twin-control/twin-control-mutation.js +85 -34
- package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
- package/dist-server/service/twin-event/twin-event.js +1 -1
- package/dist-server/service/twin-event/twin-event.js.map +1 -1
- package/dist-server/service/twin-instance/twin-instance.d.ts +1 -1
- package/dist-server/service/twin-instance/twin-instance.js +2 -2
- package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +2 -1
- package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
- package/dist-server/service/twin-model/twin-equipment.js +1 -1
- package/dist-server/service/twin-model/twin-equipment.js.map +1 -1
- package/dist-server/service/twin-model/twin-location.js +1 -1
- package/dist-server/service/twin-model/twin-location.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-query.js +1 -1
- package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
- package/dist-server/service/twin-model/twin-operation.js +1 -1
- package/dist-server/service/twin-model/twin-operation.js.map +1 -1
- package/package.json +3 -3
- package/server/engine/canonical-ingest.ts +78 -14
- package/server/engine/declared-stimulus.ts +66 -0
- package/server/engine/index.ts +2 -0
- package/server/engine/kpi-fold.ts +29 -1
- package/server/engine/kpi-query.ts +8 -8
- package/server/engine/live-feed-registry.ts +2 -2
- package/server/engine/local-declarations.ts +97 -19
- package/server/engine/measured-yield.ts +89 -0
- package/server/engine/operation-basis.ts +33 -2
- package/server/engine/property-effects.ts +17 -0
- package/server/engine/restart-policy.ts +55 -0
- package/server/engine/spec-coverage.ts +23 -3
- package/server/engine/twin-engine.ts +201 -42
- package/server/index.ts +1 -1
- package/server/service/reference/control-routing.ts +62 -0
- package/server/service/reference/index.ts +1 -0
- package/server/service/reference/reference-adapter.ts +78 -0
- package/server/service/reference/reference-live.ts +3 -3
- package/server/service/reference/reference-resolver.ts +46 -7
- package/server/service/twin-audit/command-audit.ts +34 -1
- package/server/service/twin-audit/twin-audit-event.ts +50 -4
- package/server/service/twin-control/twin-control-mutation.ts +75 -28
- package/server/service/twin-event/twin-event.ts +10 -2
- package/server/service/twin-instance/twin-instance.ts +16 -10
- package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +2 -1
- package/server/service/twin-model/twin-equipment.ts +8 -1
- package/server/service/twin-model/twin-location.ts +8 -1
- package/server/service/twin-model/twin-model-query.ts +1 -1
- package/server/service/twin-model/twin-operation.ts +8 -1
- package/test/adopt-structure-live.test.ts +7 -7
- package/test/boot-resume.test.ts +3 -3
- package/test/column-type-portability.test.ts +122 -0
- package/test/control-capability.test.ts +103 -0
- package/test/declared-stimulus.test.ts +88 -0
- package/test/ingest-running-guard.test.ts +5 -5
- package/test/instance-cache-lifecycle.test.ts +1 -1
- package/test/kpi-baseline-db.test.ts +1 -1
- package/test/kpi-query-bench.test.ts +1 -1
- package/test/lineage-survives-restart.test.ts +3 -3
- package/test/live-feed-registry.test.ts +6 -6
- package/test/local-declarations.test.ts +108 -1
- package/test/measured-yield.test.ts +90 -0
- package/test/operation-basis.test.ts +28 -1
- package/test/operational-vocabulary.test.ts +108 -0
- package/test/operations-capability-db.test.ts +4 -4
- package/test/project-structure-db.test.ts +1 -1
- package/test/projection-reaches-screen.test.ts +1 -1
- package/test/property-effects.test.ts +28 -0
- package/test/restart-policy.test.ts +111 -0
- package/test/resync-origin-site.test.ts +7 -1
- package/test/source-outcome-audit.test.ts +104 -0
- package/test/structure-revision-db.test.ts +27 -27
- package/test/tenant-registry-db.test.ts +2 -2
- package/test/twin-model-item-db.test.ts +3 -3
- package/test/twin-model-tree-db.test.ts +7 -7
- package/test/twin-origin-resync.test.ts +4 -4
- package/test/yield-loop.test.ts +144 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -374,7 +374,7 @@ export class TwinModelQuery {
|
|
|
374
374
|
})(),
|
|
375
375
|
description: inst.description ?? null,
|
|
376
376
|
kind: inst.kind,
|
|
377
|
-
|
|
377
|
+
restartPolicy: inst.restartPolicy,
|
|
378
378
|
source: ref
|
|
379
379
|
? { referenceId: ref.source, adapterType: ref.adapterType, status: ref.status, lastSyncedAt: ref.lastSyncedAt }
|
|
380
380
|
: null,
|
|
@@ -136,7 +136,14 @@ export class TwinOperation {
|
|
|
136
136
|
@Field({ description: 'Which reference (source system) this row came from. Required.' })
|
|
137
137
|
sourceRef: string
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
/*
|
|
140
|
+
* **타입 이름을 적지 않는다** (2026-08-19 Postgres 부팅 실패로 고침).
|
|
141
|
+
*
|
|
142
|
+
* `datetime` 은 sqlite·MySQL 의 이름이고 Postgres 는 `timestamp` 만 안다 — 거꾸로 `timestamp` 는
|
|
143
|
+
* sqlite 가 모른다. **다섯 드라이버에 다 통하는 이름이 없다.** 그래서 이름 고르는 일을 TypeORM 에
|
|
144
|
+
* 맡긴다(`emitDecoratorMetadata` 가 켜져 있어 `Date` 에서 드라이버별 타입을 고른다).
|
|
145
|
+
*/
|
|
146
|
+
@Column()
|
|
140
147
|
@Field({ description: 'When this row was projected from the source.' })
|
|
141
148
|
ingestedAt: Date
|
|
142
149
|
|
|
@@ -63,8 +63,8 @@ before(async () => {
|
|
|
63
63
|
const d = await ds.getRepository(Domain).save({ name: 'adopt-test', subdomain: 'adopt-test' } as any)
|
|
64
64
|
domainId = d.id
|
|
65
65
|
|
|
66
|
-
await TwinEngine.ingestMaster(domainId, master(MIRROR, 3, 2))
|
|
67
|
-
await TwinEngine.ingestMaster(domainId, master(SIM, 3, 2))
|
|
66
|
+
await TwinEngine.ingestMaster(domainId, master(MIRROR, 3, 2), 'reset')
|
|
67
|
+
await TwinEngine.ingestMaster(domainId, master(SIM, 3, 2), 'reset')
|
|
68
68
|
})
|
|
69
69
|
|
|
70
70
|
after(async () => {
|
|
@@ -74,10 +74,10 @@ after(async () => {
|
|
|
74
74
|
|
|
75
75
|
test('돌고 있는 미러는 멈추지 않고 갈아탄다 — 세 층이 함께 움직인다', async () => {
|
|
76
76
|
/* 미러 기동은 선언 기반 부팅이 가른다 — `startFromRegistry` 는 시뮬 경로다(같은 함수가 아니다). */
|
|
77
|
-
const rt = await TwinEngine.bootDeclared(domainId, MIRROR, '
|
|
77
|
+
const rt = await TwinEngine.bootDeclared(domainId, MIRROR, 'resync')
|
|
78
78
|
assert.equal(rt.mode, 'live', '미러로 떠야 이 검사가 뜻이 있다')
|
|
79
79
|
|
|
80
|
-
const res = await TwinEngine.ingestMaster(domainId, master(MIRROR, 7, 2))
|
|
80
|
+
const res = await TwinEngine.ingestMaster(domainId, master(MIRROR, 7, 2), 'reset')
|
|
81
81
|
|
|
82
82
|
/* ① 리비전 — 이후 이벤트가 달고 다닐 번호가 올라간다 */
|
|
83
83
|
const revs = await ds.getRepository(TwinStructure).find({ where: { instanceId: MIRROR }, order: { rev: 'ASC' } })
|
|
@@ -101,7 +101,7 @@ test('돌고 있는 미러는 멈추지 않고 갈아탄다 — 세 층이 함
|
|
|
101
101
|
})
|
|
102
102
|
|
|
103
103
|
test('사라진 설비를 수로 알린다 — 자리가 수천인 현장에서 직접 세어 보게 하지 않는다', async () => {
|
|
104
|
-
const res = await TwinEngine.ingestMaster(domainId, master(MIRROR, 7, 1)) // 지게차 2 → 1
|
|
104
|
+
const res = await TwinEngine.ingestMaster(domainId, master(MIRROR, 7, 1), 'reset') // 지게차 2 → 1
|
|
105
105
|
const adopted = res.warnings.find((w: any) => w.code === 'structure-adopted')
|
|
106
106
|
assert.equal(adopted.params.equipmentDropped, 1)
|
|
107
107
|
|
|
@@ -111,10 +111,10 @@ test('사라진 설비를 수로 알린다 — 자리가 수천인 현장에서
|
|
|
111
111
|
})
|
|
112
112
|
|
|
113
113
|
test('돌고 있는 시뮬레이션은 여전히 거절한다 — 조건이 바뀐 실험은 다른 실험이다', async () => {
|
|
114
|
-
await TwinEngine.bootDeclared(domainId, SIM, '
|
|
114
|
+
await TwinEngine.bootDeclared(domainId, SIM, 'reset')
|
|
115
115
|
const before = (await ds.getRepository(TwinStructure).find({ where: { instanceId: SIM } })).length
|
|
116
116
|
|
|
117
|
-
await assert.rejects(() => TwinEngine.ingestMaster(domainId, master(SIM, 9, 2)), /is running/)
|
|
117
|
+
await assert.rejects(() => TwinEngine.ingestMaster(domainId, master(SIM, 9, 2), 'reset'), /is running/)
|
|
118
118
|
|
|
119
119
|
assert.equal(
|
|
120
120
|
(await ds.getRepository(TwinStructure).find({ where: { instanceId: SIM } })).length,
|
package/test/boot-resume.test.ts
CHANGED
|
@@ -43,14 +43,14 @@ test('부팅이 런타임까지 되살린다 — 상태 복구만으로는 도
|
|
|
43
43
|
|
|
44
44
|
test('모드를 지어내지 않는다 — 미러였던 것은 미러로 되살린다', () => {
|
|
45
45
|
const body = methodOf(engine, 'resumeRow')
|
|
46
|
-
assert.match(body, /
|
|
46
|
+
assert.match(body, /restartPolicy === 'resync'/, '선언된 모드를 보고 갈라야 한다')
|
|
47
47
|
assert.match(body, /this\.startLive\(/, '미러는 관측 구동으로 세운다')
|
|
48
48
|
assert.match(body, /this\.startFromRegistry\(/, '시뮬은 등록부의 모드 그대로 세운다')
|
|
49
49
|
/*
|
|
50
50
|
* 미러 가지 안을 그대로 떠서 본다 — 미러를 시뮬로 세우면 관측 트윈이 **스스로 물건을 옮긴다**
|
|
51
51
|
* (없던 움직임을 만드는 것이다). 단어의 거리로 재면 로그·주석에 걸려 헛돈다.
|
|
52
52
|
*/
|
|
53
|
-
const at = body.indexOf("
|
|
53
|
+
const at = body.indexOf("restartPolicy === 'resync'")
|
|
54
54
|
const mirrorBranch = body.slice(at, body.indexOf('} else {', at))
|
|
55
55
|
assert.match(mirrorBranch, /startLive\(/, '미러 가지는 관측 구동으로 세워야 한다')
|
|
56
56
|
assert.equal(mirrorBranch.includes('startFromRegistry'), false, '미러 가지에서 시뮬 기동을 부른다 — 없던 움직임을 만든다')
|
|
@@ -72,7 +72,7 @@ test('벤치 사본은 무인으로 되살아나지 않는다 — 실험은 재
|
|
|
72
72
|
|
|
73
73
|
test('피드 재개는 등록부가 「도는 미러」라고 말하는 것만 — 멈춘 것을 다시 켜지 않는다', () => {
|
|
74
74
|
assert.match(live, /export async function resumeReferenceLiveFeeds/, '재개 함수가 있어야 한다')
|
|
75
|
-
assert.match(live, /r\.status === 'running' && r\.
|
|
75
|
+
assert.match(live, /r\.status === 'running' && r\.restartPolicy === 'resync'/, '도는 미러만 골라야 한다')
|
|
76
76
|
assert.match(live, /discoverSites/, '사이트 서술자는 다시 찾아서 얻는다(hint 를 남기지 않았다)')
|
|
77
77
|
/* 붙지 않은 미러는 「도는데 아무것도 오지 않는」 트윈이다 — 조용히 넘기지 않는다. */
|
|
78
78
|
assert.match(live, /NOT reattached/, '못 붙인 것은 이유와 함께 남겨야 한다')
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 컬럼 타입은 **드라이버 방언을 적지 않는다.**
|
|
3
|
+
*
|
|
4
|
+
* ── 무엇이 났나 (2026-08-19) ────────────────────────────────────────────────
|
|
5
|
+
* 릴리즈 부팅이 Postgres 에서 막혔다:
|
|
6
|
+
*
|
|
7
|
+
* DataTypeNotSupportedError: Data type "datetime" in "TwinEvent.eventTime" is not supported by "postgres"
|
|
8
|
+
*
|
|
9
|
+
* `datetime` 은 **MySQL·MSSQL 의 이름**이다(Postgres·Oracle 은 거절한다). 거꾸로 `timestamp` 는 MySQL
|
|
10
|
+
* 이 받지만 예전 주석은 「sqlite 가 지원하지 않는다」고 적어 두었다 — sqlite 드라이버는 타입 목록이
|
|
11
|
+
* 비어 있어 **아무 이름도 검증하지 않는다**(그래서 개발 중에는 무엇을 적어도 통과했다). 결국 다섯
|
|
12
|
+
* 드라이버에 다 통하는 날짜 이름은 **없다.**
|
|
13
|
+
*
|
|
14
|
+
* 그래서 이름을 고르는 일을 TypeORM 에 맡긴다: `emitDecoratorMetadata` 가 켜져 있으므로 `Date` 속성에
|
|
15
|
+
* `@Column()` 만 달면 드라이버별로 알맞게 바뀐다(Postgres `timestamp without time zone` · sqlite·MySQL
|
|
16
|
+
* `datetime` · Oracle `timestamp`) — sqlite 거동은 그대로다.
|
|
17
|
+
*
|
|
18
|
+
* 이 시험은 그 규율을 **소스에서** 지킨다: 이 패키지의 엔티티가 명시한 타입 이름은 네 드라이버(검증을
|
|
19
|
+
* 하는 쪽) 모두가 아는 것이어야 한다. 개발 DB 가 sqlite 라 아무것도 못 잡아 주기 때문에 필요하다.
|
|
20
|
+
*/
|
|
21
|
+
import { test } from 'node:test'
|
|
22
|
+
import assert from 'node:assert/strict'
|
|
23
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs'
|
|
24
|
+
import { fileURLToPath } from 'node:url'
|
|
25
|
+
import { createRequire } from 'node:module'
|
|
26
|
+
import { join } from 'node:path'
|
|
27
|
+
|
|
28
|
+
const req = createRequire(import.meta.url)
|
|
29
|
+
|
|
30
|
+
/** 드라이버가 아는 타입 이름 — **TypeORM 소스에서 읽는다**(우리가 목록을 옮겨 적지 않는다). */
|
|
31
|
+
function supportedTypes(driverPath: string): Set<string> {
|
|
32
|
+
const src = readFileSync(req.resolve(driverPath), 'utf-8')
|
|
33
|
+
const m = src.match(/supportedDataTypes = \[([\s\S]*?)\];/)
|
|
34
|
+
assert.ok(m, `${driverPath}: supportedDataTypes 를 찾지 못했다(TypeORM 구조가 바뀌었다)`)
|
|
35
|
+
return new Set((m![1].match(/"[^"]+"/g) ?? []).map(s => s.slice(1, -1)))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/*
|
|
39
|
+
* sqlite 는 목록이 비어 있다 = **검증하지 않는다.** 그래서 여기 넣지 않는다 — 넣으면 모든 타입이
|
|
40
|
+
* 「sqlite 가 모른다」로 걸려 이 시험이 소음이 된다. 개발 DB 가 아무것도 못 잡는다는 사실이 이 시험의 이유다.
|
|
41
|
+
*/
|
|
42
|
+
const DRIVERS: Record<string, string> = {
|
|
43
|
+
postgres: 'typeorm/driver/postgres/PostgresDriver.js',
|
|
44
|
+
mysql: 'typeorm/driver/mysql/MysqlDriver.js',
|
|
45
|
+
oracle: 'typeorm/driver/oracle/OracleDriver.js',
|
|
46
|
+
mssql: 'typeorm/driver/sqlserver/SqlServerDriver.js'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function entitySources(): { file: string; src: string }[] {
|
|
50
|
+
const root = fileURLToPath(new URL('../server', import.meta.url))
|
|
51
|
+
const out: { file: string; src: string }[] = []
|
|
52
|
+
const walk = (dir: string) => {
|
|
53
|
+
for (const name of readdirSync(dir)) {
|
|
54
|
+
const path = join(dir, name)
|
|
55
|
+
if (statSync(path).isDirectory()) { walk(path); continue }
|
|
56
|
+
if (!name.endsWith('.ts')) continue
|
|
57
|
+
const src = readFileSync(path, 'utf-8')
|
|
58
|
+
if (/@Entity\(/.test(src)) out.push({ file: path.slice(root.length + 1), src })
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
walk(root)
|
|
62
|
+
return out
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
test('엔티티가 있다 — 훑을 대상이 없으면 이 시험은 아무것도 지키지 않는다', () => {
|
|
66
|
+
assert.ok(entitySources().length >= 5, '이 패키지의 엔티티를 찾아야 한다')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
/*
|
|
70
|
+
* **드라이버마다 이름이 다른 컬럼은 코드가 고른다.**
|
|
71
|
+
*
|
|
72
|
+
* 긴 글이 그렇다: `text` 를 Oracle 이 모르고(그쪽은 `clob`), MySQL 계열은 `longtext`, MSSQL 은
|
|
73
|
+
* `nvarchar(MAX)` 를 쓴다. 이 레포의 정본 관행은 `DATABASE_TYPE` 으로 분기하는 것이다
|
|
74
|
+
* (`board-service` 의 `Board.model`) — 그 분기는 이 시험의 리터럴 검사에 걸리지 않는다(정상이다).
|
|
75
|
+
*
|
|
76
|
+
* 그래서 면제 목록을 두지 않는다: 고정 리터럴은 전부 잡고, 갈라야 하는 컬럼은 관행대로 가른다.
|
|
77
|
+
*/
|
|
78
|
+
const KNOWN_GAPS = new Set<string>()
|
|
79
|
+
|
|
80
|
+
test('명시한 컬럼 타입은 네 드라이버가 모두 아는 이름이다 — 개발 DB(sqlite)는 아무것도 잡아 주지 않는다', () => {
|
|
81
|
+
const supported = Object.fromEntries(Object.entries(DRIVERS).map(([n, p]) => [n, supportedTypes(p)]))
|
|
82
|
+
const violations: string[] = []
|
|
83
|
+
for (const { file, src } of entitySources()) {
|
|
84
|
+
for (const m of src.matchAll(/type:\s*'([a-z][a-z0-9 _]*)'/g)) {
|
|
85
|
+
const type = m[1]
|
|
86
|
+
const missing = Object.entries(supported).filter(([, s]) => !s.has(type)).map(([n]) => n)
|
|
87
|
+
if (!missing.length) continue
|
|
88
|
+
const line = `${file}: type '${type}' — ${missing.join(', ')} 가 모른다`
|
|
89
|
+
if (!KNOWN_GAPS.has(line)) violations.push(line)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
assert.deepEqual(violations, [], `드라이버 방언을 적은 컬럼:\n${violations.join('\n')}`)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('면제를 남겨 두면 그것이 아직 사실인지 확인한다 — 유령 면제를 남기지 않는다', () => {
|
|
96
|
+
const supported = Object.fromEntries(Object.entries(DRIVERS).map(([n, p]) => [n, supportedTypes(p)]))
|
|
97
|
+
const present = new Set<string>()
|
|
98
|
+
for (const { file, src } of entitySources()) {
|
|
99
|
+
for (const m of src.matchAll(/type:\s*'([a-z][a-z0-9 _]*)'/g)) {
|
|
100
|
+
const missing = Object.entries(supported).filter(([, s]) => !s.has(m[1])).map(([n]) => n)
|
|
101
|
+
if (missing.length) present.add(`${file}: type '${m[1]}' — ${missing.join(', ')} 가 모른다`)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
for (const gap of KNOWN_GAPS) assert.ok(present.has(gap), `이 예외는 이제 사실이 아니다(목록에서 지워라): ${gap}`)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('날짜 컬럼은 이름을 적지 않는다 — TypeORM 이 드라이버별로 고르게 둔다', () => {
|
|
108
|
+
for (const { file, src } of entitySources()) {
|
|
109
|
+
assert.equal(/type:\s*'(datetime|timestamp|timestamptz|datetime2|smalldatetime)'/.test(src), false, `${file}: 날짜 타입 이름을 적었다`)
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
/*
|
|
114
|
+
* 드라이버별로 갈라야 하는 컬럼은 **갈린 상태로** 있어야 한다 — 다시 고정 리터럴로 돌아가면 그 드라이버가
|
|
115
|
+
* 부팅에서 거절한다(감사 사유는 사람이 쓰는 글이라 길이를 자르지 않는다).
|
|
116
|
+
*/
|
|
117
|
+
test('긴 글 컬럼은 DATABASE_TYPE 으로 갈린다 — 관행을 되돌리지 않는다', () => {
|
|
118
|
+
const src = readFileSync(fileURLToPath(new URL('../server/service/twin-audit/twin-audit-event.ts', import.meta.url)), 'utf-8')
|
|
119
|
+
assert.match(src, /const DATABASE_TYPE = config\.get\('ormconfig', \{\}\)\.type/, '관행과 같은 자리에서 읽는다')
|
|
120
|
+
for (const name of ['longtext', 'clob', 'nvarchar', 'text']) assert.ok(src.includes(`'${name}'`), `${name} 갈래가 있어야 한다`)
|
|
121
|
+
assert.match(src, /length: DATABASE_TYPE == 'mssql' \? 'MAX' : undefined/, 'MSSQL 은 길이까지 말해야 잘리지 않는다')
|
|
122
|
+
})
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* **구동은 선언된 능력이다** — 그리고 원본을 지난다 (ADR-0029 §8, 2026-08-19).
|
|
3
|
+
*
|
|
4
|
+
* ── 무엇이 뚫려 있었나 ──────────────────────────────────────────────────────
|
|
5
|
+
* `controlTwinScenario` 가 트윈의 커널 런타임(`runtime.scenario`)을 **직접** 잡았다. 시뮬만 할 수 있는 일
|
|
6
|
+
* (자극·배속·정지)이 계약 밖의 지름길로 들어온 셈이고, 실 시스템에는 그 자리가 아예 없었다. ADR-0029 의
|
|
7
|
+
* 판정 문장(*"실물이 같은 계약으로 할 수 있는가"*)을 그대로 대면 그 통제면은 영원히 「규약의 결손」이 된다.
|
|
8
|
+
*
|
|
9
|
+
* 뒤집는다: 통제는 **선언된 어댑터 능력**이다. 선언하지 않은 원본에는 문이 열리지 않고, 그 사실이 결손이
|
|
10
|
+
* 아니라 사실이다(실 WMS 는 정지되지 않는다).
|
|
11
|
+
*
|
|
12
|
+
* 이 시험이 지키는 것:
|
|
13
|
+
* ① 선언과 구현이 **함께** 있어야 능력이다 — 선언만 있으면 화면이 거짓말을 하고, 구현만 있으면 닫아 둔
|
|
14
|
+
* 문을 여는 것이다(선언이 권위다).
|
|
15
|
+
* ② 능력 없는 원본은 **사유 코드로** 거절한다(조용한 실패 금지).
|
|
16
|
+
* ③ 없는 기능을 있는 것처럼 답하지 않는다(내장 시뮬은 아직 재씨앗을 못 한다).
|
|
17
|
+
*/
|
|
18
|
+
import { test } from 'node:test'
|
|
19
|
+
import assert from 'node:assert/strict'
|
|
20
|
+
import { readFileSync } from 'node:fs'
|
|
21
|
+
import { fileURLToPath } from 'node:url'
|
|
22
|
+
|
|
23
|
+
import { capabilitiesOf, type ReferenceAdapter } from '../server/service/reference/reference-adapter.ts'
|
|
24
|
+
|
|
25
|
+
const base: ReferenceAdapter = {
|
|
26
|
+
type: 'probe',
|
|
27
|
+
async testConnection() {
|
|
28
|
+
return { ok: true }
|
|
29
|
+
},
|
|
30
|
+
async discoverSites() {
|
|
31
|
+
return []
|
|
32
|
+
},
|
|
33
|
+
async fetchMaster() {
|
|
34
|
+
return { source: 'p', system: 'wms', space: { spaceId: 'p-site' }, locations: [] } as any
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
test('선언도 구현도 없으면 아무 능력이 없다 — 구조만 주는 원본이 정상이다', () => {
|
|
39
|
+
assert.deepEqual(capabilitiesOf(base), [])
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('선언만 있으면 능력이 아니다 — 화면이 「할 수 있다」고 말한 뒤 아무 일도 없게 두지 않는다', () => {
|
|
43
|
+
assert.deepEqual(capabilitiesOf({ ...base, capabilities: ['control'] }), [])
|
|
44
|
+
assert.deepEqual(capabilitiesOf({ ...base, capabilities: ['live'] }), [])
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('구현만 있으면 능력이 아니다 — 선언이 권위다(닫아 둔 문을 열지 않는다)', () => {
|
|
48
|
+
assert.deepEqual(capabilitiesOf({ ...base, control: async () => ({ ok: true }) }), [])
|
|
49
|
+
assert.deepEqual(capabilitiesOf({ ...base, openLiveFeed: () => () => {} }), [])
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('선언과 구현이 함께 있으면 그 능력이다', () => {
|
|
53
|
+
assert.deepEqual(capabilitiesOf({ ...base, capabilities: ['control'], control: async () => ({ ok: true }) }), ['control'])
|
|
54
|
+
assert.deepEqual(
|
|
55
|
+
capabilitiesOf({
|
|
56
|
+
...base,
|
|
57
|
+
capabilities: ['live', 'control'],
|
|
58
|
+
openLiveFeed: () => () => {},
|
|
59
|
+
control: async () => ({ ok: true })
|
|
60
|
+
}),
|
|
61
|
+
['live', 'control']
|
|
62
|
+
)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('어댑터가 없으면 빈손이다 — 지어내지 않는다', () => {
|
|
66
|
+
assert.deepEqual(capabilitiesOf(undefined), [])
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
/*
|
|
70
|
+
* ── 길이 하나뿐인지 (소스로 지킨다) ────────────────────────────────────────
|
|
71
|
+
* 지름길이 남아 있으면 이 결정은 문서상의 것이 된다. 그래서 ① 구동 문이 라우터를 지나고 ② 라우터가 능력을
|
|
72
|
+
* 보고 ③ 트윈 런타임을 직접 잡는 옛 경로가 사라졌는지 본다.
|
|
73
|
+
*/
|
|
74
|
+
test('구동 문이 원본을 지난다 — 트윈 런타임을 직접 잡지 않는다', () => {
|
|
75
|
+
const src = readFileSync(fileURLToPath(new URL('../server/service/twin-control/twin-control-mutation.ts', import.meta.url)), 'utf-8')
|
|
76
|
+
const code = src
|
|
77
|
+
.split('\n')
|
|
78
|
+
.filter(line => !/^\s*(\*|\/\/|\/\*)/.test(line))
|
|
79
|
+
.join('\n')
|
|
80
|
+
assert.match(code, /routeControl\(domainId, instanceId/, '구동은 라우터를 지난다')
|
|
81
|
+
assert.equal(/runtime\.scenario\./.test(code), false, '트윈의 시나리오 엔진을 직접 잡는 지름길이 없다')
|
|
82
|
+
assert.equal(/controlTwinScenario/.test(code), false, '옛 문이 남아 있지 않다')
|
|
83
|
+
assert.match(code, /twinSourceCapability/, '화면이 먼저 물을 자리가 있다')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('라우터가 선언을 본다 — 없으면 사유 코드로 거절한다', () => {
|
|
87
|
+
const src = readFileSync(fileURLToPath(new URL('../server/service/reference/control-routing.ts', import.meta.url)), 'utf-8')
|
|
88
|
+
assert.match(src, /capabilitiesOf\(adapter\)\.includes\('control'\)/)
|
|
89
|
+
assert.match(src, /'not-declared'/)
|
|
90
|
+
assert.match(src, /'no-source'/, '원본을 모르는 트윈은 구동 대상이 아니다')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('내장 시뮬은 자기가 못 하는 것을 못 한다고 말한다 — 재씨앗은 아직 없다', () => {
|
|
94
|
+
const src = readFileSync(
|
|
95
|
+
fileURLToPath(new URL('../../operato-twin/server/board/virtual-adapter.ts', import.meta.url)),
|
|
96
|
+
'utf-8'
|
|
97
|
+
)
|
|
98
|
+
/* §4 이후 이 원본은 둘을 선언한다(`live`·`control`) — 순서가 아니라 **있음**을 본다. */
|
|
99
|
+
assert.match(src, /capabilities: \[[^\]]*'control'[^\]]*\]/, '구동 능력을 선언한다')
|
|
100
|
+
assert.match(src, /capabilities: \[[^\]]*'live'[^\]]*\]/, '피드 능력도 선언한다(§4)')
|
|
101
|
+
assert.match(src, /cannot re-seed itself yet/, '없는 기능을 있는 것처럼 답하지 않는다')
|
|
102
|
+
assert.match(src, /not-running/, '아직 서지 않은 트윈에는 자극을 지어 싣지 않는다')
|
|
103
|
+
})
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 자극은 **원본이 선언한다** — 그리고 재기동에도 살아 있다.
|
|
3
|
+
*
|
|
4
|
+
* ── 무엇이 났나 (2026-08-19) ────────────────────────────────────────────────
|
|
5
|
+
* 데모의 시나리오는 시드 코드가 **메모리에만** 실었다(`runtime.scenario.load`). 그래서 트윈을 재기동하면
|
|
6
|
+
* 자극이 사라지고 구조만 서 있는 트윈이 남았다 — 작업 0·오더 0. 「살아 있는 데모」가 첫 재기동까지만
|
|
7
|
+
* 살았고, 오늘 서버를 여러 번 띄우는 동안 데모 트윈들이 실제로 그 상태였다.
|
|
8
|
+
*
|
|
9
|
+
* ── 집을 트윈이 아니라 원본으로 (ADR-0029 · plans/simulator-as-source.md §4) ─
|
|
10
|
+
* 무엇이 들어오고 무슨 주문이 나는지는 **시뮬레이터(원본)가 정하는 사실**이고 트윈은 반영한다. 트윈에
|
|
11
|
+
* 축을 만들면 ADR-0029 가 나중에 걷어내야 하고, 원본(`TwinReference.connectionConfig`)에 두면 그 계획의
|
|
12
|
+
* 첫 조각(`openLiveFeed`)이 같은 선언을 그대로 읽는다.
|
|
13
|
+
*
|
|
14
|
+
* 이 시험이 지키는 것:
|
|
15
|
+
* ① 선언이 없으면 조용히 지난다(정적 구조만 반영하는 트윈이 정상이다).
|
|
16
|
+
* ② **미러에는 싣지 않는다** — 외부 실물이 진실인 트윈에 도착을 지어 주면 그것은 현실이 아니다.
|
|
17
|
+
* ③ 검사를 통과하지 못한 선언은 **태우지 않고** 이유를 남긴다(잘못된 선언 하나가 서버를 내린 이력이 있다).
|
|
18
|
+
* ④ 검사기가 없으면 싣지 않는다 — 검사할 수 없는 선언을 태우는 것이 가장 비싼 선택이다.
|
|
19
|
+
* ⑤ 선언을 얹을 때 **옆 설정을 지우지 않는다**(접속 정보가 같은 자리에 산다).
|
|
20
|
+
*/
|
|
21
|
+
import { test } from 'node:test'
|
|
22
|
+
import assert from 'node:assert/strict'
|
|
23
|
+
import { readFileSync } from 'node:fs'
|
|
24
|
+
import { fileURLToPath } from 'node:url'
|
|
25
|
+
|
|
26
|
+
import { planStimulus, withStimulus } from '../server/engine/declared-stimulus.ts'
|
|
27
|
+
|
|
28
|
+
const scenario = { seed: 1, generators: [{ kind: 'inbound-arrival', rate: { distribution: 'constant', meanPerHour: 60 }, content: {} }] }
|
|
29
|
+
const ok = () => ({ ok: true as const })
|
|
30
|
+
const bad = () => ({ ok: false as const, errorCode: 'scenario-sku-mix-required' })
|
|
31
|
+
const sim = { hasScenarioEngine: true, mode: 'sim' as const }
|
|
32
|
+
|
|
33
|
+
test('선언이 없으면 조용히 지난다 — 구조만 반영하는 트윈이 정상이다', () => {
|
|
34
|
+
assert.deepEqual(planStimulus(undefined, sim, ok), { action: 'skip', reason: 'none' })
|
|
35
|
+
assert.deepEqual(planStimulus({ endpoint: 'x' }, sim, ok), { action: 'skip', reason: 'none' })
|
|
36
|
+
/* 시나리오 자리에 객체가 아닌 것이 오면 선언이 아니다(문자열을 시나리오로 읽지 않는다). */
|
|
37
|
+
assert.deepEqual(planStimulus({ scenario: 'daily' }, sim, ok), { action: 'skip', reason: 'none' })
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('관측으로 도는 트윈에는 싣지 않는다 — 현실을 지어내지 않는다', () => {
|
|
41
|
+
const plan = planStimulus({ scenario }, { hasScenarioEngine: true, mode: 'live' }, ok)
|
|
42
|
+
assert.deepEqual(plan, { action: 'skip', reason: 'observed' })
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('시나리오 엔진이 없으면 싣지 않는다 — 조용히 아무 일도 없게 두지 않는다', () => {
|
|
46
|
+
const plan = planStimulus({ scenario }, { hasScenarioEngine: false, mode: 'sim' }, ok)
|
|
47
|
+
assert.deepEqual(plan, { action: 'skip', reason: 'no-engine' })
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('검사를 통과하지 못한 선언은 태우지 않는다 — 사유를 든다', () => {
|
|
51
|
+
const plan = planStimulus({ scenario }, sim, bad)
|
|
52
|
+
assert.deepEqual(plan, { action: 'skip', reason: 'invalid', detail: 'scenario-sku-mix-required' })
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('검사기가 없으면 싣지 않는다 — 검사할 수 없는 선언이 가장 위험하다', () => {
|
|
56
|
+
const plan = planStimulus({ scenario }, sim, undefined)
|
|
57
|
+
assert.equal(plan.action, 'skip')
|
|
58
|
+
assert.equal((plan as any).reason, 'invalid')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('통과한 선언은 그대로 싣는다 — 값을 손대지 않는다', () => {
|
|
62
|
+
const plan = planStimulus({ scenario }, sim, ok)
|
|
63
|
+
assert.deepEqual(plan, { action: 'load', scenario })
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('선언을 얹으면서 접속 정보를 지우지 않는다', () => {
|
|
67
|
+
const merged = withStimulus({ endpoint: 'https://sim', token: 't' }, scenario)
|
|
68
|
+
assert.deepEqual(merged, { endpoint: 'https://sim', token: 't', scenario })
|
|
69
|
+
/* 설정이 없던 원본에도 얹힌다(빈 객체에서 시작). */
|
|
70
|
+
assert.deepEqual(withStimulus(undefined, scenario), { scenario })
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
/*
|
|
74
|
+
* ── 배선 (소스로 지킨다) ────────────────────────────────────────────────────
|
|
75
|
+
* 판정은 순수 함수가 하지만, 그것을 **기동 때 부르지 않으면** 아무 일도 일어나지 않는다. 그리고 못 실은
|
|
76
|
+
* 이유는 말해야 한다 — 「선언했는데 안 실렸다」가 조용하면 사용자는 트윈이 고장 난 줄로 읽는다.
|
|
77
|
+
*/
|
|
78
|
+
test('두 기동 경로에서 부른다 — 시뮬은 싣고, 미러는 그 사실을 말한다', () => {
|
|
79
|
+
const src = readFileSync(fileURLToPath(new URL('../server/engine/twin-engine.ts', import.meta.url)), 'utf-8')
|
|
80
|
+
const calls = src.match(/this\.installStimulus\(domainId, id, inst\)/g) ?? []
|
|
81
|
+
assert.equal(calls.length, 2, `기동 경로 둘에서 불러야 한다(발견 ${calls.length})`)
|
|
82
|
+
assert.match(src, /planStimulus\(config, \{ hasScenarioEngine/, '판정은 순수 함수에 맡긴다')
|
|
83
|
+
assert.match(src, /validateScenario/, '커널 검사기를 그대로 쓴다(규칙을 두 벌로 만들지 않는다)')
|
|
84
|
+
assert.match(src, /a stimulus is declared on its source but was not loaded/, '못 실은 이유를 말한다')
|
|
85
|
+
/* 선언을 적는 문도 있어야 한다 — 읽기만 있으면 아무도 선언할 수 없다. */
|
|
86
|
+
assert.match(src, /static async declareStimulus/)
|
|
87
|
+
assert.match(src, /no source reference — a stimulus has nowhere to be declared/, '원본이 없으면 지어 만들지 않는다')
|
|
88
|
+
})
|
|
@@ -86,7 +86,7 @@ before(async () => {
|
|
|
86
86
|
const d = await ds.getRepository(Domain).save({ name: 'guard-test', subdomain: 'guard-test' } as any)
|
|
87
87
|
domainId = d.id
|
|
88
88
|
|
|
89
|
-
await TwinEngine.ingestMaster(domainId, master(3, '서울 창고', '보관 구역'))
|
|
89
|
+
await TwinEngine.ingestMaster(domainId, master(3, '서울 창고', '보관 구역'), 'reset')
|
|
90
90
|
})
|
|
91
91
|
|
|
92
92
|
after(async () => {
|
|
@@ -95,7 +95,7 @@ after(async () => {
|
|
|
95
95
|
})
|
|
96
96
|
|
|
97
97
|
test('멈춰 있으면 재인제스트가 통한다 — 막는 것은 가동 중일 때뿐이다', async () => {
|
|
98
|
-
await TwinEngine.ingestMaster(domainId, master(5, '서울 창고', '보관 구역'))
|
|
98
|
+
await TwinEngine.ingestMaster(domainId, master(5, '서울 창고', '보관 구역'), 'reset')
|
|
99
99
|
const t = await trace()
|
|
100
100
|
assert.equal(t.boardLocations, 6, '자리 6개(도크 1 + 빈 5)로 갱신된다')
|
|
101
101
|
assert.deepEqual(t.revs, [1, 2], '구조가 달라졌으니 리비전이 하나 늘어난다')
|
|
@@ -103,7 +103,7 @@ test('멈춰 있으면 재인제스트가 통한다 — 막는 것은 가동 중
|
|
|
103
103
|
})
|
|
104
104
|
|
|
105
105
|
test('가동 중이면 거절하고 — **아무것도 바꾸지 않는다**', async () => {
|
|
106
|
-
await TwinEngine.startFromRegistry(domainId, INSTANCE, '
|
|
106
|
+
await TwinEngine.startFromRegistry(domainId, INSTANCE, 'reset')
|
|
107
107
|
const before = await trace()
|
|
108
108
|
|
|
109
109
|
/*
|
|
@@ -111,7 +111,7 @@ test('가동 중이면 거절하고 — **아무것도 바꾸지 않는다**', a
|
|
|
111
111
|
* 부분 쓰기는 앞쪽(공간·구역)에서 일어났으므로, 구조만 확인하면 예전 결함을 못 잡는다.
|
|
112
112
|
*/
|
|
113
113
|
await assert.rejects(
|
|
114
|
-
() => TwinEngine.ingestMaster(domainId, master(9, '부산 창고', '다른 구역')),
|
|
114
|
+
() => TwinEngine.ingestMaster(domainId, master(9, '부산 창고', '다른 구역'), 'reset'),
|
|
115
115
|
/is running/,
|
|
116
116
|
'가동 중에는 거절해야 한다'
|
|
117
117
|
)
|
|
@@ -121,7 +121,7 @@ test('가동 중이면 거절하고 — **아무것도 바꾸지 않는다**', a
|
|
|
121
121
|
|
|
122
122
|
test('멈추면 다시 통한다 — 거절은 잠금이 아니라 순서다', async () => {
|
|
123
123
|
await TwinEngine.stop(domainId, INSTANCE)
|
|
124
|
-
await TwinEngine.ingestMaster(domainId, master(9, '부산 창고', '다른 구역'))
|
|
124
|
+
await TwinEngine.ingestMaster(domainId, master(9, '부산 창고', '다른 구역'), 'reset')
|
|
125
125
|
|
|
126
126
|
const t = await trace()
|
|
127
127
|
assert.equal(t.boardLocations, 10, '아까 거절당한 그 구조가 이제 들어간다')
|
|
@@ -74,7 +74,7 @@ after(async () => {
|
|
|
74
74
|
})
|
|
75
75
|
|
|
76
76
|
test('지운 트윈의 캐시는 남지 않는다 — TTL 은 없어진 주인을 지우지 않는다', async () => {
|
|
77
|
-
await TwinEngine.ingestMaster(domainId, master())
|
|
77
|
+
await TwinEngine.ingestMaster(domainId, master(), 'reset')
|
|
78
78
|
/* 캐시를 실제로 채운다 — 손으로 넣으면 "채우는 경로" 가 바뀌어도 검사가 통과한다. */
|
|
79
79
|
await TwinEngine.structureRevOf(domainId, INSTANCE)
|
|
80
80
|
const { measured, structureRev } = caches()
|
|
@@ -90,7 +90,7 @@ before(async () => {
|
|
|
90
90
|
domain: { id: domainId },
|
|
91
91
|
instanceId: INSTANCE,
|
|
92
92
|
kind: 'wms',
|
|
93
|
-
|
|
93
|
+
restartPolicy: 'reset', /* 선언은 이제 필수 — 비워 두면 읽는 자리마다 기본값을 고른다 */
|
|
94
94
|
purpose: 'operational',
|
|
95
95
|
status: 'stopped',
|
|
96
96
|
model: { locations: [{ id: 'dock', type: 'dock', capacity: 10 }], equipment: [{ id: 'fk-1', kind: 'forklift', homeLocation: 'dock' }] }
|
|
@@ -93,7 +93,7 @@ before(async () => {
|
|
|
93
93
|
domain: { id: domainId },
|
|
94
94
|
instanceId: INSTANCE,
|
|
95
95
|
kind: 'wms',
|
|
96
|
-
|
|
96
|
+
restartPolicy: 'reset', /* 선언은 이제 필수 — 비워 두면 읽는 자리마다 기본값을 고른다 */
|
|
97
97
|
purpose: 'operational',
|
|
98
98
|
status: 'stopped',
|
|
99
99
|
model: { locations, equipment: [{ id: 'fk-1', kind: 'forklift', homeLocation: 'loc-0' }] }
|
|
@@ -124,7 +124,7 @@ before(async () => {
|
|
|
124
124
|
addDataSource('default', ds)
|
|
125
125
|
const d = await ds.getRepository(Domain).save({ name: 'lineage-test', subdomain: 'lineage-test' } as any)
|
|
126
126
|
domainId = d.id
|
|
127
|
-
await TwinEngine.ingestMaster(domainId, master())
|
|
127
|
+
await TwinEngine.ingestMaster(domainId, master(), 'reset')
|
|
128
128
|
})
|
|
129
129
|
|
|
130
130
|
after(async () => {
|
|
@@ -134,7 +134,7 @@ after(async () => {
|
|
|
134
134
|
|
|
135
135
|
test('재기동을 건너도 계보에 입력이 실린다 — 웜스타트가 확보분을 잃지 않는다', async () => {
|
|
136
136
|
/* ① 한 번 굴려 진행 중 할당을 만든다. */
|
|
137
|
-
await TwinEngine.startFromRegistry(domainId, INSTANCE, '
|
|
137
|
+
await TwinEngine.startFromRegistry(domainId, INSTANCE, 'resume')
|
|
138
138
|
drive()
|
|
139
139
|
tick(200)
|
|
140
140
|
const workingBefore = orders().filter(o => (o.allocated ?? []).length)
|
|
@@ -142,7 +142,7 @@ test('재기동을 건너도 계보에 입력이 실린다 — 웜스타트가
|
|
|
142
142
|
|
|
143
143
|
/* ② 멈추고 **저널을 재생해** 다시 세운다 — 호스트의 웜스타트 경로 그대로. */
|
|
144
144
|
await TwinEngine.stop(domainId, INSTANCE)
|
|
145
|
-
await TwinEngine.startFromRegistry(domainId, INSTANCE, '
|
|
145
|
+
await TwinEngine.startFromRegistry(domainId, INSTANCE, 'resume')
|
|
146
146
|
|
|
147
147
|
const revived = orders().filter(o => (o.allocated ?? []).length)
|
|
148
148
|
assert.ok(revived.length, '되살아난 오더가 확보분을 들고 있어야 한다 — 잃으면 계보의 입력이 빈다')
|
|
@@ -41,15 +41,15 @@ test('답하지 못하는 장부가 다른 계층의 사실을 죽이지 않는
|
|
|
41
41
|
|
|
42
42
|
test('「없음」과 「해당 없음」을 섞지 않는다', () => {
|
|
43
43
|
/* 도는 미러 — 이 둘이 우리가 보이려던 구별이다. */
|
|
44
|
-
assert.equal(liveFeedStateOf({
|
|
45
|
-
assert.equal(liveFeedStateOf({
|
|
44
|
+
assert.equal(liveFeedStateOf({ restartPolicy: 'resync', running: true, instanceId: 'ems-plant-1' }), 'attached')
|
|
45
|
+
assert.equal(liveFeedStateOf({ restartPolicy: 'resync', running: true, instanceId: 'ems-plant-2' }), 'detached')
|
|
46
46
|
|
|
47
47
|
/* 시뮬 트윈에는 나를 피드가 **없어야 한다** — 결함이 아니라 그 트윈의 성질이다. */
|
|
48
|
-
assert.equal(liveFeedStateOf({
|
|
49
|
-
assert.equal(liveFeedStateOf({
|
|
48
|
+
assert.equal(liveFeedStateOf({ restartPolicy: 'resume', running: true, instanceId: 'hatio-us' }), 'not-applicable')
|
|
49
|
+
assert.equal(liveFeedStateOf({ restartPolicy: 'reset', running: true, instanceId: 'trailer-new' }), 'not-applicable')
|
|
50
50
|
|
|
51
51
|
/* 사람이 멈춰 둔 트윈에게 피드를 묻는 것은 뜻이 없다. */
|
|
52
|
-
assert.equal(liveFeedStateOf({
|
|
52
|
+
assert.equal(liveFeedStateOf({ restartPolicy: 'resync', running: false, instanceId: 'ems-plant-1' }), 'not-applicable')
|
|
53
53
|
|
|
54
54
|
/* 선언이 없으면 미러라고 짐작하지 않는다(짐작으로 경보를 울리지 않는다). */
|
|
55
55
|
assert.equal(liveFeedStateOf({ running: true, instanceId: 'ems-plant-1' }), 'not-applicable')
|
|
@@ -63,5 +63,5 @@ test('목록이 그 사실을 함께 낸다 — 화면이 따로 물어야 하
|
|
|
63
63
|
assert.ok(at >= 0, 'list() 가 없다 — 이름이 바뀌었으면 이 검사도 함께 고친다')
|
|
64
64
|
const body = src.slice(at, src.indexOf('\n /** 운영 공간 목록', at))
|
|
65
65
|
assert.match(body, /liveFeed: liveFeedStateOf\(/, '관리 목록 행에 피드 사실이 실려야 한다')
|
|
66
|
-
assert.match(body, /
|
|
66
|
+
assert.match(body, /restartPolicy: r\.restartPolicy/, '선언된 모드를 넘겨야 한다(짐작하지 않는다)')
|
|
67
67
|
})
|