@zq-silk/yui 0.6.3 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -881,94 +881,19 @@ atomic storage switch, purely to make that single switch recoverable — it is n
|
|
|
881
881
|
a general backup/restore facility.)
|
|
882
882
|
|
|
883
883
|
See [ARCHITECTURE.md](./ARCHITECTURE.md) for persistence and scheduling details.
|
|
884
|
-
The reusable, user-driven acceptance plan is documented in
|
|
885
|
-
[Operator routing and long-running Task E2E testing](./docs/testing/operator-routing-e2e-plan.md).
|
|
886
|
-
|
|
887
884
|
## Development
|
|
888
885
|
|
|
889
886
|
```sh
|
|
890
887
|
npm ci
|
|
891
|
-
npm
|
|
892
|
-
```
|
|
893
|
-
|
|
894
|
-
`npm run test:core` builds once and runs the same small, explicit core matrix as
|
|
895
|
-
CI. During development, add the smallest affected test files for the behavior
|
|
896
|
-
being changed.
|
|
897
|
-
|
|
898
|
-
`npm test` (and `make test` / `make check`) remains the full **deterministic**
|
|
899
|
-
diagnostic suite. Run it on demand for unusually cross-cutting changes or
|
|
900
|
-
regression investigation; it is not a routine merge requirement. It never
|
|
901
|
-
launches a real model or touches the global `yui` binary, a shared `YUI_HOME`,
|
|
902
|
-
or a running production Session. It stays
|
|
903
|
-
deterministic even when launched from inside a managed Yui Session, because it
|
|
904
|
-
preloads `test/helpers/scrubSessionEnv.js` to strip every Yui-owned managed
|
|
905
|
-
runtime value from the test process, including shared `YUI_HOME`, exact Leader
|
|
906
|
-
action assertions, workspace projections, and Agent launch descriptors. Tests
|
|
907
|
-
that touch Home/CLI/Controller/tmux explicitly supply a test-created isolated
|
|
908
|
-
Home. The same preamble puts local refusal shims for bare `codex` and `claude`
|
|
909
|
-
ahead of the caller's `PATH`; Session fixtures install observable Mock Agents
|
|
910
|
-
inside their owned Home instead. Only a dedicated managed-identity child may
|
|
911
|
-
opt out. The Provider E2E tier is exempt from the shims only after its explicit
|
|
912
|
-
opt-in and mandatory isolation preflight path has been selected.
|
|
913
|
-
|
|
914
|
-
### Test tiers
|
|
915
|
-
|
|
916
|
-
Yui's tests are classified into five explicit, executable tiers so a reader
|
|
917
|
-
never has to guess what a test actually exercised. Each tier declares whether it
|
|
918
|
-
creates a Session, whether it calls a real model, and whether it stands up a
|
|
919
|
-
disposable real runtime. Agent workflow for applying these tiers while developing
|
|
920
|
-
Yui lives in [`.agents/skills/develop-yui/SKILL.md`](.agents/skills/develop-yui/SKILL.md); it is
|
|
921
|
-
not part of the generic Leader, Worker, or Reviewer workflow:
|
|
922
|
-
|
|
923
|
-
| Tier | Session | Real model | Disposable runtime | Preflight | Opt-in |
|
|
924
|
-
| --- | --- | --- | --- | --- | --- |
|
|
925
|
-
| Unit | no | no | no | no | — |
|
|
926
|
-
| Isolated Integration | yes | no | yes | no | — |
|
|
927
|
-
| Mock Agent Session | yes | no | yes | no | — |
|
|
928
|
-
| Provider E2E | yes | **yes** | yes | **required** | `YUI_ALLOW_PROVIDER_E2E=1` |
|
|
929
|
-
| Release E2E | **no** | **no** | yes | **required** | `YUI_ALLOW_RELEASE_E2E=1` |
|
|
930
|
-
|
|
931
|
-
```sh
|
|
932
|
-
make test-tier T=unit # or: npm run test:tier -- unit
|
|
933
|
-
npm run test:tier -- unit -- --test-name-pattern "test name"
|
|
934
|
-
node scripts/run-test-tier.mjs list
|
|
888
|
+
npm test
|
|
935
889
|
```
|
|
936
890
|
|
|
937
|
-
The
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
normal path, creates no Session and calls no model — it exercises
|
|
944
|
-
binary/install/update/upgrade release flows against real npm/home/namespace
|
|
945
|
-
resources. Both tiers are **privileged and fail-closed**: they live only in
|
|
946
|
-
nested privileged manifests excluded from the default test glob, refuse to run
|
|
947
|
-
without their opt-in env var, and execute through one wrapper that registers
|
|
948
|
-
cleanup before observation and does not even evaluate the scenario module until
|
|
949
|
-
the blocking isolation preflight (`assertIsolationReady`) passes. Active-Session
|
|
950
|
-
observation is runner-owned and uses an all-scope Yui runtime inventory;
|
|
951
|
-
scenario code cannot replace it or manufacture an empty result. The preflight
|
|
952
|
-
requires an absolute
|
|
953
|
-
checkout-local launcher; a run root proven **temporary and creator-bound owned by
|
|
954
|
-
this run** — created via `createOwnedRunRoot` (mkdtemp + a random-token
|
|
955
|
-
ownership receipt) and re-proven by that exact token, with a symlink run root
|
|
956
|
-
refused and every path canonicalized so a symlink escape cannot pass a lexical
|
|
957
|
-
check; the disposable `YUI_HOME`, workspace, isolated npm prefix, and unique
|
|
958
|
-
runtime namespace all derived *inside that exact owned root* and **physically
|
|
959
|
-
fenced** against symlink escape; and an **explicit** observation that zero
|
|
960
|
-
production Sessions are active (missing evidence fails closed — it is never
|
|
961
|
-
assumed empty). No bare `yui`, `make link` symlink, shared home, arbitrary or
|
|
962
|
-
pre-existing foreign run root, symlinked path, or unproven Session state is
|
|
963
|
-
tolerated. Real-runtime teardown scans and cleans only the creator-owned Home,
|
|
964
|
-
uses Yui's exact process/pane/artifact identity fences, verifies the Home-derived
|
|
965
|
-
tmux server is absent, and refuses environment overrides that redirect
|
|
966
|
-
`YUI_HOME`. The reusable annotated-resource selector separately requires an
|
|
967
|
-
exact non-empty creator token plus matching `ephemeral-test` marker; a missing
|
|
968
|
-
token touches nothing and is a failed cleanup outcome. **Mock Agent Session
|
|
969
|
-
transport success does not prove
|
|
970
|
-
provider-native acceptance** — only the Provider E2E tier can record that. See
|
|
971
|
-
[docs/testing/test-tiers.md](./docs/testing/test-tiers.md) for the full contract.
|
|
891
|
+
The permanent suite is intentionally one seconds-scale core smoke. It checks
|
|
892
|
+
CLI startup, a normal SQLite Task path, the supported migration graph, and the
|
|
893
|
+
built-in Agent Drivers. Change-specific TDD fixtures and abnormal-data repros
|
|
894
|
+
are temporary development evidence and are removed when the change is complete;
|
|
895
|
+
they do not accumulate as permanent regression tests. See
|
|
896
|
+
[the verification policy](./docs/testing/verification-levels.md).
|
|
972
897
|
|
|
973
898
|
To make user terminals use this checkout, reversibly link the user-level `yui` command:
|
|
974
899
|
|
|
@@ -1303,24 +1303,68 @@ function requireAggregateV17Snapshot(snapshot) {
|
|
|
1303
1303
|
/**
|
|
1304
1304
|
* Runtime state is now projected exclusively from canonical
|
|
1305
1305
|
* `runtime.observation` events. Offline upgrade inventory proves there are no
|
|
1306
|
-
* active Runs or live Sessions
|
|
1307
|
-
*
|
|
1306
|
+
* active Runs or live Sessions. A retired Task has also explicitly abandoned
|
|
1307
|
+
* its runtime, so its stored runtime inconsistencies no longer block the Home;
|
|
1308
|
+
* the anomalous records themselves remain available as history. Non-retired
|
|
1309
|
+
* Tasks still fail closed and receive the supported retirement command.
|
|
1308
1310
|
*/
|
|
1309
1311
|
function migrateAggregateV18ToV19(snapshot) {
|
|
1310
1312
|
requireAggregateV18Snapshot(snapshot);
|
|
1313
|
+
const schemaManifest = {
|
|
1314
|
+
...snapshot.schemaManifest,
|
|
1315
|
+
aggregateSchemaVersion: RUNTIME_OBSERVATION_AGGREGATE_TO_VERSION
|
|
1316
|
+
};
|
|
1317
|
+
if (snapshot.state === null)
|
|
1318
|
+
return { schemaManifest, state: null };
|
|
1319
|
+
const tasks = asObject(snapshot.state.tasks, "state tasks");
|
|
1320
|
+
const nextTasks = {};
|
|
1321
|
+
for (const [taskId, rawStoredTask] of Object.entries(tasks)) {
|
|
1322
|
+
const storedTask = asObject(rawStoredTask, `Task aggregate ${taskId}`);
|
|
1323
|
+
const rawTask = storedTask.task;
|
|
1324
|
+
const task = rawTask !== null && typeof rawTask === "object" && !Array.isArray(rawTask)
|
|
1325
|
+
? rawTask
|
|
1326
|
+
: undefined;
|
|
1327
|
+
if (task?.status === "retired") {
|
|
1328
|
+
nextTasks[taskId] = { ...storedTask };
|
|
1329
|
+
continue;
|
|
1330
|
+
}
|
|
1331
|
+
requireResolvableActiveRunPointers(taskId, storedTask);
|
|
1332
|
+
nextTasks[taskId] = { ...storedTask };
|
|
1333
|
+
}
|
|
1311
1334
|
return {
|
|
1312
|
-
schemaManifest
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
: {
|
|
1319
|
-
...snapshot.state,
|
|
1320
|
-
schemaVersion: RUNTIME_OBSERVATION_AGGREGATE_TO_VERSION
|
|
1321
|
-
}
|
|
1335
|
+
schemaManifest,
|
|
1336
|
+
state: {
|
|
1337
|
+
...snapshot.state,
|
|
1338
|
+
schemaVersion: RUNTIME_OBSERVATION_AGGREGATE_TO_VERSION,
|
|
1339
|
+
tasks: nextTasks
|
|
1340
|
+
}
|
|
1322
1341
|
};
|
|
1323
1342
|
}
|
|
1343
|
+
function requireResolvableActiveRunPointers(taskId, storedTask) {
|
|
1344
|
+
if (storedTask.activeRuns === undefined)
|
|
1345
|
+
return;
|
|
1346
|
+
const activeRuns = asObject(storedTask.activeRuns, `activeRunPointer map ${taskId}`);
|
|
1347
|
+
if (Object.keys(activeRuns).length === 0)
|
|
1348
|
+
return;
|
|
1349
|
+
const agentRuns = asObject(storedTask.agentRuns, `agentRun map ${taskId}`);
|
|
1350
|
+
for (const [pointer, rawActiveRun] of Object.entries(activeRuns)) {
|
|
1351
|
+
const activeRun = asObject(rawActiveRun, `Active run ${taskId}/${pointer}`);
|
|
1352
|
+
const runId = typeof activeRun.runId === "string" ? activeRun.runId.trim() : "";
|
|
1353
|
+
if (runId.length === 0) {
|
|
1354
|
+
throw new Error(`Active run pointer ${taskId}/${pointer} has an invalid runId. `
|
|
1355
|
+
+ taskRetirementUpgradeHint(taskId));
|
|
1356
|
+
}
|
|
1357
|
+
if (agentRuns[runId] === undefined) {
|
|
1358
|
+
throw new Error(`Active run pointer ${taskId}/${pointer} references missing agent run ${runId}. `
|
|
1359
|
+
+ taskRetirementUpgradeHint(taskId));
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
function taskRetirementUpgradeHint(taskId) {
|
|
1364
|
+
return `Retire Task ${taskId} with `
|
|
1365
|
+
+ `\`yui task retire ${taskId} --summary "abandon inconsistent runtime state"\`, `
|
|
1366
|
+
+ "then retry `yui update`.";
|
|
1367
|
+
}
|
|
1324
1368
|
function requireAggregateV18Snapshot(snapshot) {
|
|
1325
1369
|
if (snapshot.schemaManifest.aggregateSchemaVersion
|
|
1326
1370
|
!== RUNTIME_OBSERVATION_AGGREGATE_FROM_VERSION) {
|
|
@@ -37,7 +37,7 @@ import { planMigration } from "../migration/planner.js";
|
|
|
37
37
|
import { AmbiguousSwitchError } from "../migration/index.js";
|
|
38
38
|
import { describeActiveRuntime, homeRuntimeIsActive, inspectHomeRuntime, inspectSourceVersionState, inspectSnapshotVersionState } from "./homeMigrationTarget.js";
|
|
39
39
|
import { writeSwitchProgress } from "./switchProgress.js";
|
|
40
|
-
import { COMMITTED_DATABASE_FILENAME, STAGED_DATABASE_FILENAME, computeDbFamilyChecksums, computeStateFamilyChecksums, populateSqliteFromState, readStateFromSqlite } from "./sqliteStateMigration.js";
|
|
40
|
+
import { COMMITTED_DATABASE_FILENAME, STAGED_DATABASE_FILENAME, copySqlitePassthroughState, computeDbFamilyChecksums, computeStateFamilyChecksums, populateSqliteFromState, readStateFromSqlite } from "./sqliteStateMigration.js";
|
|
41
41
|
/** Build the SQLite-backed record-migration target. */
|
|
42
42
|
export function createSqliteRecordMigrationTarget(options) {
|
|
43
43
|
const home = options.home;
|
|
@@ -92,6 +92,7 @@ export function createSqliteRecordMigrationTarget(options) {
|
|
|
92
92
|
updatedAt: now().toISOString()
|
|
93
93
|
};
|
|
94
94
|
populateSqliteFromState(home, snapshot.state ?? {}, STAGED_DATABASE_FILENAME);
|
|
95
|
+
copySqlitePassthroughState(home, COMMITTED_DATABASE_FILENAME, STAGED_DATABASE_FILENAME);
|
|
95
96
|
},
|
|
96
97
|
rebuildDerivedState(effects) {
|
|
97
98
|
// The SQLite database is fully normalised by populateSqliteFromState;
|
|
@@ -33,6 +33,90 @@ import { CURRENT_STORED_TASK_SCHEMA_VERSION } from "../taskStore.js";
|
|
|
33
33
|
export const STAGED_DATABASE_FILENAME = "yui.db.staged";
|
|
34
34
|
/** The committed database filename. */
|
|
35
35
|
export const COMMITTED_DATABASE_FILENAME = "yui.db";
|
|
36
|
+
/**
|
|
37
|
+
* Preserve SQLite-owned durable state that intentionally sits outside the
|
|
38
|
+
* state.json-shaped Task aggregate snapshot. Volatile coordination locks and
|
|
39
|
+
* derived projections are rebuilt or dropped at the offline boundary.
|
|
40
|
+
*/
|
|
41
|
+
export function copySqlitePassthroughState(home, sourceDatabaseFilename, targetDatabaseFilename) {
|
|
42
|
+
if (sourceDatabaseFilename === targetDatabaseFilename) {
|
|
43
|
+
throw new Error("SQLite passthrough copy requires distinct source and target databases.");
|
|
44
|
+
}
|
|
45
|
+
const source = new Database(join(home, sourceDatabaseFilename), { readonly: true });
|
|
46
|
+
const target = new Database(join(home, targetDatabaseFilename));
|
|
47
|
+
try {
|
|
48
|
+
target.pragma("foreign_keys = ON");
|
|
49
|
+
target.transaction(() => {
|
|
50
|
+
mergeGlobalSequences(source, target);
|
|
51
|
+
copyTableRows(source, target, "outbox");
|
|
52
|
+
copyMailboxSignals(source, target);
|
|
53
|
+
copyTableRows(source, target, "work_item_candidates");
|
|
54
|
+
copyTableRows(source, target, "review_findings");
|
|
55
|
+
copyTableRows(source, target, "telemetry");
|
|
56
|
+
if (sqliteTableExists(source, "telemetry_aggregate")) {
|
|
57
|
+
target.exec("DELETE FROM telemetry_aggregate");
|
|
58
|
+
copyTableRows(source, target, "telemetry_aggregate");
|
|
59
|
+
}
|
|
60
|
+
copyTableRows(source, target, "session_owners");
|
|
61
|
+
copyTableRows(source, target, "resource_registry");
|
|
62
|
+
copyTableRows(source, target, "gate_artifacts");
|
|
63
|
+
copyTableRows(source, target, "gate_artifact_logs");
|
|
64
|
+
})();
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
source.close();
|
|
68
|
+
target.close();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function mergeGlobalSequences(source, target) {
|
|
72
|
+
if (!sqliteTableExists(source, "global_sequences"))
|
|
73
|
+
return;
|
|
74
|
+
const rows = source.prepare("SELECT name, high_water FROM global_sequences").all();
|
|
75
|
+
const merge = target.prepare(`INSERT INTO global_sequences (name, high_water) VALUES (?, ?)
|
|
76
|
+
ON CONFLICT(name) DO UPDATE SET
|
|
77
|
+
high_water = MAX(global_sequences.high_water, excluded.high_water)`);
|
|
78
|
+
for (const row of rows)
|
|
79
|
+
merge.run(row.name, row.high_water);
|
|
80
|
+
}
|
|
81
|
+
function copyMailboxSignals(source, target) {
|
|
82
|
+
if (!sqliteTableExists(source, "mailbox_signals"))
|
|
83
|
+
return;
|
|
84
|
+
const rows = source.prepare(`SELECT m.target_key, s.sequence, s.reason, s.ref_type, s.ref_task_id,
|
|
85
|
+
s.ref_id, s.occurred_at, s.request_id
|
|
86
|
+
FROM mailbox_signals s
|
|
87
|
+
JOIN mailboxes m ON m.mailbox_id = s.mailbox_id
|
|
88
|
+
ORDER BY m.target_key, s.sequence`).iterate();
|
|
89
|
+
const findMailbox = target.prepare("SELECT mailbox_id FROM mailboxes WHERE target_key = ?");
|
|
90
|
+
const insert = target.prepare(`INSERT INTO mailbox_signals
|
|
91
|
+
(mailbox_id, sequence, reason, ref_type, ref_task_id, ref_id, occurred_at, request_id)
|
|
92
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
93
|
+
for (const row of rows) {
|
|
94
|
+
const mailbox = findMailbox.get(row.target_key);
|
|
95
|
+
if (mailbox === undefined) {
|
|
96
|
+
throw new Error(`SQLite migration cannot preserve signals for missing mailbox ${row.target_key}.`);
|
|
97
|
+
}
|
|
98
|
+
insert.run(mailbox.mailbox_id, row.sequence, row.reason, row.ref_type, row.ref_task_id, row.ref_id, row.occurred_at, row.request_id);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function copyTableRows(source, target, table) {
|
|
102
|
+
if (!sqliteTableExists(source, table) || !sqliteTableExists(target, table))
|
|
103
|
+
return;
|
|
104
|
+
const columns = source.prepare(`PRAGMA table_info(${quoteSqliteIdentifier(table)})`).all().map((row) => row.name);
|
|
105
|
+
if (columns.length === 0)
|
|
106
|
+
return;
|
|
107
|
+
const quotedColumns = columns.map(quoteSqliteIdentifier);
|
|
108
|
+
const rows = source.prepare(`SELECT ${quotedColumns.join(", ")} FROM ${quoteSqliteIdentifier(table)}`).iterate();
|
|
109
|
+
const insert = target.prepare(`INSERT INTO ${quoteSqliteIdentifier(table)} (${quotedColumns.join(", ")}) `
|
|
110
|
+
+ `VALUES (${columns.map(() => "?").join(", ")})`);
|
|
111
|
+
for (const row of rows)
|
|
112
|
+
insert.run(...columns.map((column) => row[column]));
|
|
113
|
+
}
|
|
114
|
+
function sqliteTableExists(db, table) {
|
|
115
|
+
return db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(table) !== undefined;
|
|
116
|
+
}
|
|
117
|
+
function quoteSqliteIdentifier(value) {
|
|
118
|
+
return `"${value.replaceAll("\"", "\"\"")}"`;
|
|
119
|
+
}
|
|
36
120
|
// ---------------------------------------------------------------------------
|
|
37
121
|
// Canonical JSON and hashing
|
|
38
122
|
// ---------------------------------------------------------------------------
|
|
@@ -157,6 +241,7 @@ function tasksOf(state) {
|
|
|
157
241
|
* sidecar database file.
|
|
158
242
|
*/
|
|
159
243
|
export function populateSqliteFromState(home, state, databaseFilename) {
|
|
244
|
+
const retiredActiveRuns = [];
|
|
160
245
|
const store = new SqliteTaskStore(home, { databaseFilename, migration: true });
|
|
161
246
|
try {
|
|
162
247
|
store.transaction(() => {
|
|
@@ -234,6 +319,17 @@ export function populateSqliteFromState(home, state, databaseFilename) {
|
|
|
234
319
|
// Active-run pointers: the document stores { schemaVersion, runId }
|
|
235
320
|
// keyed by pointer; the store derives the pointer from the Run.
|
|
236
321
|
for (const [pointer, value] of Object.entries(stored.activeRuns)) {
|
|
322
|
+
if (stored.task.status === "retired") {
|
|
323
|
+
retiredActiveRuns.push({
|
|
324
|
+
taskId,
|
|
325
|
+
pointer,
|
|
326
|
+
value,
|
|
327
|
+
updatedAt: typeof stored.task.updatedAt === "string"
|
|
328
|
+
? stored.task.updatedAt
|
|
329
|
+
: new Date().toISOString()
|
|
330
|
+
});
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
237
333
|
const run = stored.agentRuns[value.runId];
|
|
238
334
|
if (run === undefined) {
|
|
239
335
|
throw new Error(`Active run pointer ${taskId}/${pointer} references missing agent run ${value.runId}.`);
|
|
@@ -291,6 +387,33 @@ export function populateSqliteFromState(home, state, databaseFilename) {
|
|
|
291
387
|
finally {
|
|
292
388
|
store.close();
|
|
293
389
|
}
|
|
390
|
+
persistRetiredActiveRunPointers(home, databaseFilename, retiredActiveRuns);
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* A retired Task is an explicit isolation boundary. Its active-run rows are
|
|
394
|
+
* retained byte-for-byte at the logical record level even when their Run is
|
|
395
|
+
* missing; normal Tasks continue through the referentially strict store path.
|
|
396
|
+
*/
|
|
397
|
+
function persistRetiredActiveRunPointers(home, databaseFilename, pointers) {
|
|
398
|
+
if (pointers.length === 0)
|
|
399
|
+
return;
|
|
400
|
+
const db = new Database(join(home, databaseFilename));
|
|
401
|
+
try {
|
|
402
|
+
const insert = db.prepare(`INSERT INTO active_runs (task_id, pointer, run_id, payload, updated_at)
|
|
403
|
+
VALUES (?, ?, ?, ?, ?)`);
|
|
404
|
+
db.transaction(() => {
|
|
405
|
+
for (const entry of pointers) {
|
|
406
|
+
if (typeof entry.value.runId !== "string") {
|
|
407
|
+
throw new Error(`Retired Task active run pointer ${entry.taskId}/${entry.pointer} `
|
|
408
|
+
+ "cannot be represented because runId is not a string.");
|
|
409
|
+
}
|
|
410
|
+
insert.run(entry.taskId, entry.pointer, entry.value.runId, JSON.stringify(entry.value), entry.updatedAt);
|
|
411
|
+
}
|
|
412
|
+
})();
|
|
413
|
+
}
|
|
414
|
+
finally {
|
|
415
|
+
db.close();
|
|
416
|
+
}
|
|
294
417
|
}
|
|
295
418
|
/**
|
|
296
419
|
* Seed `global_sequences` from the numeric suffixes of existing task and
|
package/i18n/README.zh-CN.md
CHANGED
|
@@ -534,8 +534,6 @@ Agent 环境变量绑定只保存进程环境变量名,不保存 secret 值;
|
|
|
534
534
|
Yui 面向一台机器上的一个受信任本地用户。它的 Web/API 仅支持 loopback,不包含远程或多用户 Web、分布式协调、backup/import/export、trash/restore、derived index、recovery journal、runtime lease、inactivity TTL、cooldown 或 recurring schedule。
|
|
535
535
|
|
|
536
536
|
持久化和调度细节见 [ARCHITECTURE.md](../ARCHITECTURE.md)。
|
|
537
|
-
可复用的用户视角验收方案见
|
|
538
|
-
[Operator 路由与长期任务端到端测试方案](../docs/testing/operator-routing-e2e-plan.md)。
|
|
539
537
|
|
|
540
538
|
## 本地开发
|
|
541
539
|
|
|
@@ -546,6 +544,11 @@ npm test
|
|
|
546
544
|
npm run lint
|
|
547
545
|
```
|
|
548
546
|
|
|
547
|
+
`npm test` 只保留秒级核心 smoke:CLI 启动、正常 SQLite Task、受支持迁移和内置
|
|
548
|
+
Agent Driver。针对当前修改编写的 TDD、异常数据和故障复现仅作为开发期证据,需求完成后
|
|
549
|
+
删除,不累积为常驻回归测试。具体约束见
|
|
550
|
+
[验证策略](../docs/testing/verification-levels.md)。
|
|
551
|
+
|
|
549
552
|
如需让用户终端使用当前 checkout,可逆地接管用户级 `yui` 命令:
|
|
550
553
|
|
|
551
554
|
```sh
|