task-write-unit.test.mjs
140 lines 4.4 KB
Raw
sha256:baa800aedf841dbf32081aa7b2befa288ac33dfc7175ac55014c55c4d8c742f9 docs: move durable-auth freeze/evidence to local developmen… Human 11 days ago
1 /**
2 * Tier 1 — UNIT: taskStateId/loopStateId, gating, kind routing.
3 */
4 import { describe, it, beforeEach, afterEach } from 'node:test';
5 import assert from 'node:assert/strict';
6 import fs from 'node:fs';
7 import path from 'node:path';
8 import { fileURLToPath } from 'node:url';
9
10 import {
11 taskStateId,
12 loopStateId,
13 absentTaskStateId,
14 absentLoopStateId,
15 getTaskWritesEnabled,
16 handleTaskProposeRequest,
17 computeMaterializeTaskId,
18 computeLazyOccurrenceKey,
19 TASK_STATE_ID_PREFIX,
20 LOOP_STATE_ID_PREFIX,
21 TASK_PROPOSAL_SCHEMA,
22 } from '../lib/task/task-write.mjs';
23 import { createProposal } from '../hub/proposals-store.mjs';
24 import { sampleTaskCreatePayload } from './fixtures/task/write-helpers.mjs';
25
26 const __dirname = path.dirname(fileURLToPath(import.meta.url));
27 const tmpRoot = path.join(__dirname, 'fixtures', 'tmp-task-write-unit');
28
29 describe('taskStateId / loopStateId — deterministic tokens', () => {
30 it('taskStateId is stable and prefixed', async () => {
31 const task = {
32 schema: 'knowtation.task/v0',
33 task_id: 'task_x',
34 kind: 'personal',
35 scope: 'personal',
36 status: 'pending',
37 title: 'T',
38 workspace_id: 'ws',
39 due_at: null,
40 assignee_ref: null,
41 assigner_ref: null,
42 run_ref: null,
43 loop_ref: null,
44 occurrence_key: null,
45 occurrence_at: null,
46 series_status_snapshot: null,
47 skip_reason: null,
48 artifact_links: [],
49 };
50 const a = taskStateId(task);
51 assert.equal(a, taskStateId(task));
52 assert.ok(a.startsWith(TASK_STATE_ID_PREFIX));
53 });
54
55 it('absent sentinels are stable', async () => {
56 assert.equal(absentTaskStateId(), absentTaskStateId());
57 assert.equal(absentLoopStateId(), absentLoopStateId());
58 assert.ok(absentLoopStateId().startsWith(LOOP_STATE_ID_PREFIX));
59 });
60
61 it('computeMaterializeTaskId matches school-trip fixture shape', async () => {
62 const r = computeMaterializeTaskId('loop_school_trip', '2026-W25');
63 assert.equal(r.ok, true);
64 assert.equal(r.taskId, 'task_school_trip_2026_w25');
65 });
66 });
67
68 describe('gating — TASK_WRITES_ENABLED default OFF', () => {
69 const dataDir = path.join(tmpRoot, 'gate');
70 beforeEach(() => {
71 fs.rmSync(tmpRoot, { recursive: true, force: true });
72 fs.mkdirSync(dataDir, { recursive: true });
73 delete process.env.TASK_WRITES_ENABLED;
74 });
75 afterEach(() => {
76 delete process.env.TASK_WRITES_ENABLED;
77 });
78
79 it('defaults disabled', async () => {
80 assert.equal(getTaskWritesEnabled(dataDir), false);
81 });
82
83 it('disabled propose returns TASK_WRITES_DISABLED', async () => {
84 const result = await handleTaskProposeRequest({
85 dataDir,
86 vaultId: 'default',
87 cliScopes: ['personal'],
88 proposalKind: 'task_create',
89 body: sampleTaskCreatePayload(),
90 intent: 'test',
91 createProposal,
92 });
93 assert.equal(result.ok, false);
94 assert.equal(result.code, 'TASK_WRITES_DISABLED');
95 });
96
97 it('enabled via env proposes envelope shape', async () => {
98 process.env.TASK_WRITES_ENABLED = '1';
99 const result = await handleTaskProposeRequest({
100 dataDir,
101 vaultId: 'default',
102 cliScopes: ['personal'],
103 proposalKind: 'task_create',
104 body: sampleTaskCreatePayload(),
105 intent: 'test',
106 createProposal,
107 });
108 assert.equal(result.ok, true);
109 assert.equal(result.payload.schema, TASK_PROPOSAL_SCHEMA);
110 assert.equal(result.payload.auto_approvable, false);
111 assert.equal(result.payload.review_queue, 'task-writes');
112 });
113
114 it('async createProposal injector resolves proposal_id (hosted bridge parity)', async () => {
115 process.env.TASK_WRITES_ENABLED = '1';
116 const result = await handleTaskProposeRequest({
117 dataDir,
118 vaultId: 'default',
119 cliScopes: ['personal'],
120 proposalKind: 'task_create',
121 body: sampleTaskCreatePayload(),
122 intent: 'async hosted',
123 createProposal: async (_dataDir, input) => ({
124 proposal_id: 'prop_async_hosted_001',
125 path: input.path,
126 status: 'proposed',
127 }),
128 });
129 assert.equal(result.ok, true);
130 assert.equal(result.payload.proposal_id, 'prop_async_hosted_001');
131 });
132 });
133
134 describe('computeLazyOccurrenceKey — manual recurrence', () => {
135 it('increments manual-N keys', async () => {
136 const loop = { recurrence: { kind: 'manual' } };
137 const keys = new Set(['manual-1']);
138 assert.equal(computeLazyOccurrenceKey(loop, keys), 'manual-2');
139 });
140 });
File History 1 commit
sha256:baa800aedf841dbf32081aa7b2befa288ac33dfc7175ac55014c55c4d8c742f9 docs: move durable-auth freeze/evidence to local developmen… Human 11 days ago